diff --git a/CHANGES.md b/CHANGES.md index 17d225e..99c39c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - The delay for a transition can now be an expression rather than just a float value. This allows for more dynamic transitions. For example the delay can now be a random value (using `randf_range()`) or any expression property. Of course you can still just use a single float number. This change is backwards-compatible, all existing state charts will automatically be converted to the new format when loaded. There is a new example named `random_transitions` which shows this new feature to create a randomly wandering mob. A big thanks goes out to [Miguel Silva](https://github.com/mrjshzk) and [alextkd2003](https://github.com/alextkd2003) for providing POC PRs for this feature. +- It is now possible to read expression properties back from the state chart. This is useful for debugging or for avoiding to hold the same value in multiple places ([#110](https://github.com/derkork/godot-statecharts/issues/110)). +- It is now possible to set initial values for expression properties in the state chart. This avoids getting errors when using expressions in transitions that run immediately after the state chart is started and the expression property has not been set yet. This is again backwards-compatible, all existing state charts will automatically start with an empty dictionary of expression properties. ### Improved - The state chart debugger in the editor now automatically selects the first state chart when the game starts. This reduces the amount of clicking needed to start debugging a state chart ([#118](https://github.com/derkork/godot-statecharts/issues/118)). diff --git a/addons/godot_state_charts/csharp/NodeWrapper.cs b/addons/godot_state_charts/csharp/NodeWrapper.cs index f2f477c..d7ebd5f 100644 --- a/addons/godot_state_charts/csharp/NodeWrapper.cs +++ b/addons/godot_state_charts/csharp/NodeWrapper.cs @@ -24,26 +24,26 @@ protected NodeWrapper(Node wrapped) /// /// /// - public void Connect(StringName signal, Callable method, uint flags = 0u) + public Error Connect(StringName signal, Callable method, uint flags = 0u) { - Wrapped.Connect(signal, method, flags); + return Wrapped.Connect(signal, method, flags); } /// /// Allows to call methods on the wrapped node deferred. /// - public void CallDeferred(string method, params Variant[] args) + public Variant CallDeferred(string method, params Variant[] args) { - Wrapped.CallDeferred(method, args); + return Wrapped.CallDeferred(method, args); } /// /// Allows to call methods on the wrapped node. /// - public void Call(string method, params Variant[] args) + public Variant Call(string method, params Variant[] args) { - Wrapped.Call(method, args); + return Wrapped.Call(method, args); } } } diff --git a/addons/godot_state_charts/csharp/StateChart.cs b/addons/godot_state_charts/csharp/StateChart.cs index f1f224d..5a6d1e4 100644 --- a/addons/godot_state_charts/csharp/StateChart.cs +++ b/addons/godot_state_charts/csharp/StateChart.cs @@ -51,6 +51,18 @@ public void SetExpressionProperty(string name, Variant value) { Call(MethodName.SetExpressionProperty, name, value); } + + + /// + /// Returns the value of an expression property on the state chart node. + /// + /// the name of the proeprty to read. This is case sensitive. + /// the default value to be returned if no such property exists + /// the value of the property + public T GetExpressionProperty<[MustBeVariant]T>(string name, T defaultValue = default) + { + return Call(MethodName.GetExpressionProperty, name, Variant.From(defaultValue)).As(); + } /// /// Steps the state chart node. This will invoke all state_stepped signals on the @@ -91,6 +103,11 @@ public class SignalName : Node.SignalName /// public static readonly StringName SetExpressionProperty = "set_expression_property"; + /// + /// Returns the value of an expression property on the state chart node. + /// + public static readonly StringName GetExpressionProperty = "get_expression_property"; + /// /// Steps the state chart node. This will invoke all state_stepped signals on the /// currently active states in the state charts. See the "Stepping Mode" section of the manual diff --git a/addons/godot_state_charts/state_chart.gd b/addons/godot_state_charts/state_chart.gd index daba5a7..f080ea3 100644 --- a/addons/godot_state_charts/state_chart.gd +++ b/addons/godot_state_charts/state_chart.gd @@ -24,6 +24,13 @@ signal event_received(event:StringName) ## state chart debugger in the editor. @export var track_in_editor:bool = false +## Initial values for the expression properties. These properties can be used in expressions, e.g +## for guards or transition delays. It is recommended to set an initial value for each property +## you use in an expression to ensure that this expression is always valid. If you don't set +## an initial value, some expressions may fail to be evaluated if they use a property that has +## not been set yet. +@export var initial_expression_properties:Dictionary = {} + ## The root state of the state chart. var _state:StateChartState = null @@ -63,6 +70,14 @@ func _ready() -> void: if not child is StateChartState: push_error("StateMachine's child must be a State") return + + # set the initial expression properties + if initial_expression_properties != null: + for key in initial_expression_properties.keys(): + if not key is String and not key is StringName: + push_error("Expression property names must be strings. Ignoring initial expression property with key ", key) + continue + _expression_properties[key] = initial_expression_properties[key] # initialize the state machine _state = child as StateChartState @@ -111,7 +126,13 @@ func set_expression_property(name:StringName, value) -> void: _property_change_pending = true _run_changes() - + +## Returns the value of a previously set expression property. If the property does not exist, the default value +## will be returned. +func get_expression_property(name:StringName, default:Variant = null) -> Variant: + return _expression_properties.get(name, default) + + func _run_changes() -> void: if _locked_down: return diff --git a/godot-state-charts.csproj b/godot-state-charts.csproj index 8a353ec..e255488 100644 --- a/godot-state-charts.csproj +++ b/godot-state-charts.csproj @@ -272,6 +272,7 @@ + diff --git a/godot_state_charts_examples/csharp/CSharpExample.cs b/godot_state_charts_examples/csharp/CSharpExample.cs index 97cf3d4..999b853 100644 --- a/godot_state_charts_examples/csharp/CSharpExample.cs +++ b/godot_state_charts_examples/csharp/CSharpExample.cs @@ -11,7 +11,6 @@ public partial class CSharpExample : Node2D private StateChart _stateChart; private Label _feelLabel; - private int _poisonCount; private int _health = 20; private StateChartState _poisonedStateChartState; @@ -35,10 +34,11 @@ public override void _Ready() /// private void OnDrinkPoisonButtonPressed() { - _poisonCount += 3; // we add three rounds worth of poison - // This uses the regular API to interact with the state chart. - _stateChart.SetExpressionProperty("poison_count", _poisonCount); + var currentPoisonCount = _stateChart.GetExpressionProperty("poison_count", 0); + currentPoisonCount += 3; // we add three rounds worth of poison + + _stateChart.SetExpressionProperty("poison_count", currentPoisonCount); _stateChart.SendEvent("poisoned"); // Ends the round @@ -50,8 +50,6 @@ private void OnDrinkPoisonButtonPressed() /// private void OnDrinkCureButtonPressed() { - _poisonCount = 0; - // Here we use some custom-made extension methods from StateChartExt.cs to have a nicer API // that is specific to our game. This avoids having to use strings for property names and // event names and it also helps with type safety and when you need to find all places where @@ -81,8 +79,7 @@ private void EndRound() _stateChart.Step(); // Then at the beginning of the next round, we reduce any poison count by 1 - _poisonCount = Mathf.Max(0, _poisonCount - 1); - _stateChart.SetPoisonCount(_poisonCount); + _stateChart.SetPoisonCount( Mathf.Max(0, _stateChart.GetPoisonCount() - 1)); // And update the UI RefreshUi(); @@ -91,7 +88,7 @@ private void EndRound() private void OnPoisonedStateStepped() { // when we step while poisoned, remove the amount of poison from our health (but not below 0) - _health = Mathf.Max(0, _health - _poisonCount); + _health = Mathf.Max(0, _health - _stateChart.GetPoisonCount()); } private void OnNormalStateStepped() @@ -103,7 +100,7 @@ private void OnNormalStateStepped() private void RefreshUi() { - _feelLabel.Text = $"Health: {_health} Poison: {_poisonCount}"; + _feelLabel.Text = $"Health: {_health} Poison: {_stateChart.GetPoisonCount()}"; } private void OnDebugButtonPressed() diff --git a/godot_state_charts_examples/csharp/StateChartExt.cs b/godot_state_charts_examples/csharp/StateChartExt.cs index 88ef7ce..ae13b6c 100644 --- a/godot_state_charts_examples/csharp/StateChartExt.cs +++ b/godot_state_charts_examples/csharp/StateChartExt.cs @@ -13,6 +13,11 @@ public static void SetPoisonCount(this StateChart stateChart, int poisonCount) stateChart.SetExpressionProperty("poison_count", poisonCount); } + public static int GetPoisonCount(this StateChart stateChart) + { + return stateChart.GetExpressionProperty("poison_count", 0); + } + public static void SendCuredEvent(this StateChart stateChart) { stateChart.SendEvent("cured"); diff --git a/tests/framework/state_chart_test_base.gd b/tests/framework/state_chart_test_base.gd index a25de23..f0b63f3 100644 --- a/tests/framework/state_chart_test_base.gd +++ b/tests/framework/state_chart_test_base.gd @@ -11,6 +11,13 @@ func send_event(event: String) -> void: func set_expression_property(name: String, value: Variant) -> void: _chart.set_expression_property(name, value) +@warning_ignore("shadowed_variable_base_class") +func get_expression_property(name: String, default:Variant = null) -> Variant: + return _chart.get_expression_property(name, default) + +func set_initial_expression_properties(properties: Dictionary) -> void: + _chart.initial_expression_properties = properties + func step()-> void: _chart.step() diff --git a/tests/test_automatic_transition_on_property_change.gd b/tests/test_automatic_transition_on_property_change.gd index e135d75..ff534d0 100644 --- a/tests/test_automatic_transition_on_property_change.gd +++ b/tests/test_automatic_transition_on_property_change.gd @@ -2,6 +2,9 @@ extends StateChartTestBase func test_automatic_transition_on_property_change(): + # Ensure that we have an initial value for the property, to avoid error messages. + set_initial_expression_properties({"x": 0}) + var root := compound_state("root") var a := atomic_state("a", root) var b := atomic_state("b", root) diff --git a/tests/test_expression_properties.gd b/tests/test_expression_properties.gd new file mode 100644 index 0000000..7eca0c9 --- /dev/null +++ b/tests/test_expression_properties.gd @@ -0,0 +1,23 @@ +extends StateChartTestBase + +func test_expression_properties(): + # set some initial expression properties + set_initial_expression_properties({ + "foo": "bar", + "baz": "qux" + }) + + # some dummy states, we don't need them + var root := compound_state("root") + atomic_state("a", root) + + await finish_setup() + + # the initial expression properties should be set + assert_eq(get_expression_property("foo"), "bar") + + # if we set a new expression property + set_expression_property("foo", "baz") + + # the new value should be returned + assert_eq(get_expression_property("foo"), "baz")