diff --git a/CHANGES.md b/CHANGES.md index c9378ab..e40c7ec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.17.0] - 2024-08-16 +### Added +- The C# wrappers now provide type-safe events for all signals that the underlying nodes emit. This way you can simply subscribe to a signal using the familiar `+=` notation, e.g. `stateChart.StateEntered += OnStateEntered`. This makes it easier to work with the state chart from C# code. A big thanks goes out to [Marques Lévy](https://github.com/Prakkkmak) for suggesting this feature and providing a POC PR for it ([#126](https://github.com/derkork/godot-statecharts/pull/126)). Note that the usual rules for signals in C# apply, e.g. signal connections will not automatically be disconnected when the receiver is freed. +- ### Fixed - The library now handles cases better where code tries to access a state chart that has been removed from the tree. This may happen when using Godot's `change_scene_to_file` or `change_scene_to_packed` functions. Debug output in these cases will no longer try to get full path names of nodes that have been removed from the tree. This should prevent errors and crashes in these cases ([#129](https://github.com/derkork/godot-statecharts/issues/129)). - The error messages for evaluating expressions have been improved. They now show the expression that was evaluated and the result of the evaluation ([#138](https://github.com/derkork/godot-statecharts/issues/138)) diff --git a/addons/godot_state_charts/csharp/CompoundState.cs b/addons/godot_state_charts/csharp/CompoundState.cs index 5cc76bc..fdef872 100644 --- a/addons/godot_state_charts/csharp/CompoundState.cs +++ b/addons/godot_state_charts/csharp/CompoundState.cs @@ -11,7 +11,24 @@ namespace GodotStateCharts /// public class CompoundState : StateChartState { - + /// + /// Called when a child state is entered. + /// + public event Action ChildStateEntered + { + add => Wrapped.Connect(SignalName.ChildStateEntered, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.ChildStateEntered, Callable.From(value)); + } + + /// + /// Called when a child state is exited. + /// + public event Action ChildStateExited + { + add => Wrapped.Connect(SignalName.ChildStateExited, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.ChildStateExited, Callable.From(value)); + } + private CompoundState(Node wrapped) : base(wrapped) { } @@ -37,14 +54,11 @@ private CompoundState(Node wrapped) : base(wrapped) public new class SignalName : StateChartState.SignalName { - /// - /// Called when a child state is entered. - /// + + /// public static readonly StringName ChildStateEntered = "child_state_entered"; - /// - /// Called when a child state is exited. - /// + /// public static readonly StringName ChildStateExited = "child_state_exited"; } diff --git a/addons/godot_state_charts/csharp/StateChart.cs b/addons/godot_state_charts/csharp/StateChart.cs index 8af76b5..fdcb785 100644 --- a/addons/godot_state_charts/csharp/StateChart.cs +++ b/addons/godot_state_charts/csharp/StateChart.cs @@ -10,11 +10,26 @@ namespace GodotStateCharts /// public class StateChart : NodeWrapper { - public TypeSafeSignal EventReceived { get; private set; } + /// + /// Emitted when the state chart receives an event. This will be + /// emitted no matter which state is currently active and can be + /// useful to trigger additional logic elsewhere in the game + /// without having to create a custom event bus. It is also used + /// by the state chart debugger. Note that this will emit the + /// events in the order in which they are processed, which may + /// be different from the order in which they were received. This is + /// because the state chart will always finish processing one event + /// fully before processing the next. If an event is received + /// while another is still processing, it will be enqueued. + /// + public event Action EventReceived + { + add => Wrapped.Connect(SignalName.EventReceived, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.EventReceived, Callable.From(value)); + } protected StateChart(Node wrapped) : base(wrapped) { - EventReceived = new TypeSafeSignal(Wrapped, SignalName.EventReceived); } /// @@ -79,18 +94,9 @@ public void Step() public class SignalName : Node.SignalName { - /// - /// Emitted when the state chart receives an event. This will be - /// emitted no matter which state is currently active and can be - /// useful to trigger additional logic elsewhere in the game - /// without having to create a custom event bus. It is also used - /// by the state chart debugger. Note that this will emit the - /// events in the order in which they are processed, which may - /// be different from the order in which they were received. This is - /// because the state chart will always finish processing one event - /// fully before processing the next. If an event is received - /// while another is still processing, it will be enqueued. - /// + /// + /// + /// public static readonly StringName EventReceived = "event_received"; } diff --git a/addons/godot_state_charts/csharp/StateChartState.cs b/addons/godot_state_charts/csharp/StateChartState.cs index a9bdc26..d718174 100644 --- a/addons/godot_state_charts/csharp/StateChartState.cs +++ b/addons/godot_state_charts/csharp/StateChartState.cs @@ -10,33 +10,91 @@ namespace GodotStateCharts /// public class StateChartState : NodeWrapper { - public TypeSafeSignal StateEntered { get; } - public TypeSafeSignal StateExited { get; } - public TypeSafeSignal> EventReceived { get; } + /// + /// Called when the state is entered. + /// + public event Action StateEntered + { + add => Wrapped.Connect(SignalName.StateEntered, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.StateEntered, Callable.From(value)); + } - public TypeSafeSignal> StateProcessing { get; } - public TypeSafeSignal> StatePhysicsProcessing { get; } + /// + /// Called when the state is exited. + /// + public event Action StateExited + { + add => Wrapped.Connect(SignalName.StateExited, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.StateExited, Callable.From(value)); + } - public TypeSafeSignal StateStepped { get; } + /// + /// Called when the state receives an event. Only called if the state is active. + /// + public event Action EventReceived + { + add => Wrapped.Connect(SignalName.EventReceived, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.EventReceived, Callable.From(value)); + } + + /// + /// Called when the state is processing. + /// + public event Action StateProcessing + { + add => Wrapped.Connect(SignalName.StateProcessing, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.StateProcessing, Callable.From(value)); + } - public TypeSafeSignal> StateInput { get; } + /// + /// Called when the state is physics processing. + /// + public event Action StatePhysicsProcessing + { + add => Wrapped.Connect(SignalName.StatePhysicsProcessing, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.StatePhysicsProcessing, Callable.From(value)); + } - public TypeSafeSignal> StateUnhandledInput { get; } + /// + /// Called when the state chart Step function is called. + /// + public event Action StateStepped + { + add => Wrapped.Connect(SignalName.StateStepped, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.StateStepped, Callable.From(value)); + } - public TypeSafeSignal> TransitionPending { get; } - - protected StateChartState(Node wrapped) : base(wrapped) + /// + /// Called when the state is receiving input. + /// + public event Action StateInput + { + add => Wrapped.Connect(SignalName.StateInput, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.StateInput, Callable.From(value)); + } + + /// + /// Called when the state is receiving unhandled input. + /// + public event Action StateUnhandledInput { - StateEntered = new TypeSafeSignal(Wrapped, SignalName.StateEntered); - StateExited = new TypeSafeSignal(Wrapped, SignalName.StateExited); - EventReceived = new TypeSafeSignal>(Wrapped, SignalName.EventReceived); - StateProcessing = new TypeSafeSignal>(Wrapped, SignalName.StateProcessing); - StatePhysicsProcessing = new TypeSafeSignal>(Wrapped, SignalName.StatePhysicsProcessing); - StateStepped = new TypeSafeSignal(Wrapped, SignalName.StateStepped); - StateInput = new TypeSafeSignal>(Wrapped, SignalName.StateInput); - StateUnhandledInput = new TypeSafeSignal>(Wrapped, SignalName.StateUnhandledInput); - TransitionPending = new TypeSafeSignal>(Wrapped, SignalName.TransitionPending); + add => Wrapped.Connect(SignalName.StateUnhandledInput, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.StateUnhandledInput, Callable.From(value)); } + + /// + /// Called every frame while a delayed transition is pending for this state. + /// Returns the initial delay and the remaining delay of the transition. + /// + public event Action TransitionPending + { + add => Wrapped.Connect(SignalName.TransitionPending, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.TransitionPending, Callable.From(value)); + } + + + protected StateChartState(Node wrapped) : base(wrapped) {} + /// /// Creates a wrapper object around the given node and verifies that the node @@ -66,51 +124,31 @@ public static StateChartState Of(Node state) public class SignalName : Godot.Node.SignalName { - /// - /// Called when the state is entered. - /// + /// public static readonly StringName StateEntered = "state_entered"; - /// - /// Called when the state is exited. - /// + /// public static readonly StringName StateExited = "state_exited"; - /// - /// Called when the state receives an event. Only called if the state is active. - /// + /// public static readonly StringName EventReceived = "event_received"; - /// - /// Called when the state is processing. - /// + /// public static readonly StringName StateProcessing = "state_processing"; - - /// - /// Called when the state is physics processing. - /// + + /// public static readonly StringName StatePhysicsProcessing = "state_physics_processing"; - /// - /// Called when the state chart Step function is called. - /// + /// public static readonly StringName StateStepped = "state_stepped"; - /// - /// Called when the state is receiving input. - /// + /// public static readonly StringName StateInput = "state_input"; - - /// - /// Called when the state is receiving unhandled input. - /// + /// public static readonly StringName StateUnhandledInput = "state_unhandled_input"; - /// - /// Called every frame while a delayed transition is pending for this state. - /// Returns the initial delay and the remaining delay of the transition. - /// + /// public static readonly StringName TransitionPending = "transition_pending"; } diff --git a/addons/godot_state_charts/csharp/Transition.cs b/addons/godot_state_charts/csharp/Transition.cs index e19a5ca..94f84a2 100644 --- a/addons/godot_state_charts/csharp/Transition.cs +++ b/addons/godot_state_charts/csharp/Transition.cs @@ -1,18 +1,37 @@ +using System; + namespace GodotStateCharts { using Godot; /// - /// A transition between two states. This class only exists to make the - /// signal names available in C#. It is not intended to be instantiated - /// or otherwise used. + /// A transition between two states. /// - public class Transition { + public class Transition : NodeWrapper { + + /// + /// Called when the transition is taken. + /// + public event Action Taken { + add => Wrapped.Connect(SignalName.Taken, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.Taken, Callable.From(value)); + } + + private Transition(Node transition) : base(transition) {} + + public static Transition Of(Node transition) { + if (transition.GetScript().As