Skip to content

Commit

Permalink
Restructure Switch [closes #134] (#137)
Browse files Browse the repository at this point in the history
So there was a bug where child scenes that have signals emitted from and
then connected to themselves would have their signals connected to the
**parent** scene that was hosted them. This was tracked upstream in
godotengine/godot#48064 (comment).

I reworked the switch component to have relevant signals either not
connected via auto-wiring in the scene or to be emitted / connected to
not the root object. It's kinda trash and more confusing but (I think 😰)
it will work and as long as we don't have to come back and make
substantial changes it should be "fine"

Hello tech debt.

While here I setup all moveable blocks to be included in the
switch-actor collision layer by default because even I was forgetting to
set that up.

---------

Co-authored-by: envy <this-is-envy@users.noreply.github.com>
  • Loading branch information
this-is-envy and this-is-envy authored Dec 11, 2024
1 parent 2099a11 commit 7a7a07b
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 160 deletions.
1 change: 1 addition & 0 deletions Scenes/Components/Greyboxing/MoveableBlock.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ resource_local_to_scene = true
size = Vector2(32, 32)

[node name="MoveableBlock" type="RigidBody2D" groups=["NodesWithID"]]
collision_layer = 5
mass = 0.25
gravity_scale = 0.0
lock_rotation = true
Expand Down
26 changes: 14 additions & 12 deletions Scenes/Components/SimpleSwitch.tscn
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
[gd_scene load_steps=4 format=3 uid="uid://bbdfk08t5dt1b"]
[gd_scene load_steps=5 format=3 uid="uid://c3fld6my8so3p"]

[ext_resource type="PackedScene" uid="uid://bmwu7t5fqfmd5" path="res://Scenes/Components/Switch.tscn" id="1_a5k7c"]
[ext_resource type="Script" path="res://Scripts/Components/SimpleSwitch.gd" id="2_xotxe"]
[ext_resource type="PackedScene" uid="uid://bmwu7t5fqfmd5" path="res://Scenes/Components/Switch.tscn" id="1_8we76"]
[ext_resource type="Script" path="res://Scripts/Components/SimpleSwitchConfig.gd" id="2_l3rma"]
[ext_resource type="Script" path="res://Scripts/Components/SimpleSwitchSignalDelegate.gd" id="3_lnafs"]

[sub_resource type="RectangleShape2D" id="RectangleShape2D_e1puu"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ytkac"]
resource_local_to_scene = true
size = Vector2(0, 0)

[node name="SimpleSwitch" instance=ExtResource("1_a5k7c")]
z_index = -1
script = ExtResource("2_xotxe")
[node name="SimpleSwitchParent" instance=ExtResource("1_8we76")]
script = ExtResource("2_l3rma")
track_variable = ""
sensor_size = Vector2(0, 0)
feedback_enabled = false
default_color = Color(0.745098, 0.745098, 0.745098, 1)
active_color = Color(0, 0.392157, 0, 1)

[node name="Polygon2D" type="Polygon2D" parent="." index="0"]
[node name="SignalDelegate" type="Node" parent="." index="0"]
script = ExtResource("3_lnafs")

[node name="Polygon2D" type="Polygon2D" parent="Switch" index="0"]
visible = false
color = Color(0.745098, 0.745098, 0.745098, 1)
polygon = PackedVector2Array(0, 0, 0, 0, 0, 0, 0, 0)

[node name="CollisionShape2D" type="CollisionShape2D" parent="." index="1"]
shape = SubResource("RectangleShape2D_e1puu")

[connection signal="triggered" from="." to="." method="_on_triggered"]
[node name="CollisionShape2D" type="CollisionShape2D" parent="Switch" index="1"]
shape = SubResource("RectangleShape2D_ytkac")
16 changes: 10 additions & 6 deletions Scenes/Components/Switch.tscn
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
[gd_scene load_steps=2 format=3 uid="uid://bmwu7t5fqfmd5"]
[gd_scene load_steps=3 format=3 uid="uid://bmwu7t5fqfmd5"]

[ext_resource type="Script" path="res://Scripts/Components/SwitchConfig.gd" id="1_fdbxp"]
[ext_resource type="Script" path="res://Scripts/Components/Switch.gd" id="1_rlyjj"]

[node name="Switch" type="Area2D"]
[node name="SwitchParent" type="Node2D"]
script = ExtResource("1_fdbxp")

[node name="Switch" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 4
script = ExtResource("1_rlyjj")

[connection signal="area_entered" from="." to="." method="_on_enter"]
[connection signal="area_exited" from="." to="." method="_on_exit"]
[connection signal="body_entered" from="." to="." method="_on_enter_body"]
[connection signal="body_exited" from="." to="." method="_on_exit_body"]
[connection signal="area_entered" from="Switch" to="Switch" method="_on_enter"]
[connection signal="area_exited" from="Switch" to="Switch" method="_on_exit"]
[connection signal="body_entered" from="Switch" to="Switch" method="_on_enter_body"]
[connection signal="body_exited" from="Switch" to="Switch" method="_on_exit_body"]
161 changes: 122 additions & 39 deletions Scenes/Levels/BadLevelA.tscn

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Scripts/Components/LevelBase.gd
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const DEFAULT_MARKER: String = "PlayerStart"
return editor_overlay_color
set(value):
editor_overlay_color = value
if Engine.is_editor_hint():
if Engine.is_editor_hint() && _canvas_modulate != null:
_canvas_modulate.color = value

## When enabled the color overlay will be applied. When unset it will not.
Expand All @@ -20,7 +20,7 @@ const DEFAULT_MARKER: String = "PlayerStart"
return apply_editor_overlay
set(value):
apply_editor_overlay = value
if Engine.is_editor_hint():
if Engine.is_editor_hint() && _canvas_modulate != null:
_canvas_modulate.visible = apply_editor_overlay

var driver: Driver
Expand All @@ -29,11 +29,11 @@ var level_name: String = name

var _canvas_modulate: CanvasModulate = null:
get:
var path := "CanwasModulate"
var path := "CanvasModulate"
if has_node(path):
# done as a property getter because we can't use @onready as part of @tool
# script and I don't know a better pattern
return get_node("CanvasModulate")
return get_node(path)
return null

@onready var _marker_root := $Markers
Expand Down
50 changes: 0 additions & 50 deletions Scripts/Components/SimpleSwitch.gd

This file was deleted.

69 changes: 69 additions & 0 deletions Scripts/Components/SimpleSwitchConfig.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@tool
class_name SimpleSwitchConfig
extends SwitchConfig

@export var track_variable: String = ""
@export var sensor_size: Vector2:
set(value):
sensor_size = value
_sync_sensor()

@export_category("Visual Feedback")
@export var feedback_enabled := false:
set(value):
feedback_enabled = value
_sync_feedback()

@export var default_color := Color.GRAY
@export var active_color := Color.DARK_GREEN

var _delegate: Node
var _switch_poly: Polygon2D
var _switch_shape: CollisionShape2D


func _ready() -> void:
super()

_switch = $Switch
_switch_poly = $Switch/Polygon2D
_switch_shape = $Switch/CollisionShape2D
_delegate = $SignalDelegate

if !Engine.is_editor_hint():
_delegate.configure(self, _switch_poly)

_sync_feedback()
_sync_sensor()


func _process(_delta: float) -> void:
if Engine.is_editor_hint():
_switch_poly.visible = feedback_enabled
return

if track_variable != "":
# TODO: this is probably not super performant but good enough for now
_delegate.set_activation(Dialogic.VAR.get_variable(track_variable))


func _sync_feedback() -> void:
if _switch_poly != null:
_switch_poly.visible = feedback_enabled


func _sync_sensor() -> void:
var x := sensor_size.x / 2
var y := sensor_size.y / 2
if _switch_poly != null:
_switch_poly.polygon = [
Vector2(-x, -y),
Vector2(x, -y),
Vector2(x, y),
Vector2(-x, y),
]
if _switch_shape != null:
# TODO: is conversion finished?
var rs2d := RectangleShape2D.new()
rs2d.size = sensor_size
_switch_shape.shape = rs2d
26 changes: 26 additions & 0 deletions Scripts/Components/SimpleSwitchSignalDelegate.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class_name SimpleSwitchSignalDelegate
extends Node

var _cfg: SimpleSwitchConfig
var _poly: Polygon2D


func configure(cfg: SimpleSwitchConfig, switch_poly: Polygon2D) -> void:
_cfg = cfg
_poly = switch_poly
if !_cfg.triggered.is_connected(_on_triggered):
_cfg.triggered.connect(_on_triggered)

## set initial pressed state based on the current state of the switch
set_activation(cfg.is_pressed)


func _on_triggered(_id: String, state: bool) -> void:
set_activation(state)


func set_activation(state: bool) -> void:
if state:
_poly.color = _cfg.active_color
else:
_poly.color = _cfg.default_color
63 changes: 19 additions & 44 deletions Scripts/Components/Switch.gd
Original file line number Diff line number Diff line change
Expand Up @@ -22,47 +22,22 @@
class_name Switch
extends Area2D

signal triggered(id: String, state: bool)
signal failed_trigger(id: String, reason: Enums.TriggerFailure)

## Checked as part of an evaluation if some actor can press a switch
@export var conditions: Array[TriggerCondition] = []

## If set a switch may only be triggered once and will not be released when
## the trigger actors are removed. May be reset only via [reset].
@export var single_fire: bool = false

## If set only an actor with one of the listed IDs can activate a switch.
@export var id_mask: Array[String] = []

## A list of effects to perform when a switch is pressed
@export var on_pressed_effects: Array[Effect] = []
## A list of effects to perform when a switch is released
@export var on_released_effects: Array[Effect] = []

## This is set when the switch has been pressed by one or more actors
var is_pressed: bool = false

## Tracks the full set of ids that are present in this switch area and have
## triggered an activation (or can keep it activated)
var _activation_stack: Array[String]

## Tracks the level that the action is taking place in
var _cur_level: LevelBase

var _config: SwitchConfig


func _ready() -> void:
_cur_level = Utils.get_level_parent(self)


## Reset switch state. That means:
## a. clears the activation stack
## b. sets is_pressed to false.
## Does not emit triggered(false) or activate on_released_effects chain. If a
## switch was previously single_fire it remains single_fire after a reset.
func reset() -> void:
_activation_stack.clear()
is_pressed = false
func set_config(cfg: SwitchConfig) -> void:
_config = cfg


func _on_enter(area: Area2D) -> void:
Expand Down Expand Up @@ -94,26 +69,26 @@ func _on_exit_body(body: Node2D) -> void:


func _on_enter_id(id: String) -> void:
if id_mask != null && id_mask.size() > 0:
if !(id in id_mask):
if _config.id_mask != null && _config.id_mask.size() > 0:
if !(id in _config.id_mask):
# Because the switch is already pressed this activation didn't really
# fail to press it so much it didn't get into the activation stack.
if !is_pressed:
failed_trigger.emit(id, Enums.TriggerFailure.ID_MASK)
if !_config.is_pressed:
_config.failed_trigger.emit(id, Enums.TriggerFailure.ID_MASK)
return

if id in _activation_stack:
printerr("%s: ID %s attempting to activate but already in stack" % [name, id])
return

for c in conditions:
for c in _config.conditions:
if !c.evaluate(id):
failed_trigger.emit(id, Enums.TriggerFailure.CONDITIONS)
_config.failed_trigger.emit(id, Enums.TriggerFailure.CONDITIONS)
return

_activation_stack.push_back(id)

if !is_pressed:
if !_config.is_pressed:
_do_press(id)


Expand All @@ -131,22 +106,22 @@ func _on_exit_id(id: String) -> void:
## internal state, emits triggered signals, and fires any configured effects.
## If a switch is already pressed bails without any changes / side effects.
func _do_press(id: String) -> void:
if is_pressed:
if _config.is_pressed:
return
is_pressed = true
triggered.emit(id, true)
for e in on_pressed_effects:
_config.is_pressed = true
_config.triggered.emit(id, true)
for e in _config.on_pressed_effects:
e.act(id, _cur_level)


## Does the work of deactivating the switch. That is it updates internal state,
## emits a triggered signal, and fires any configured effects. If a switch is
## single_fire then it does not make any changes.
func _do_release(id: String) -> void:
if single_fire:
if _config.single_fire:
return

is_pressed = false
triggered.emit(id, false)
for e in on_released_effects:
_config.is_pressed = false
_config.triggered.emit(id, false)
for e in _config.on_released_effects:
e.act(id, _cur_level)
Loading

0 comments on commit 7a7a07b

Please sign in to comment.