Skip to content

Commit

Permalink
feat: provide type safe signal wrappers for C#
Browse files Browse the repository at this point in the history
fixes #126, #130
  • Loading branch information
derkork committed Aug 16, 2024
1 parent 077ef45 commit 0eb4fc8
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 161 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
28 changes: 21 additions & 7 deletions addons/godot_state_charts/csharp/CompoundState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,24 @@ namespace GodotStateCharts
/// </summary>
public class CompoundState : StateChartState
{

/// <summary>
/// Called when a child state is entered.
/// </summary>
public event Action ChildStateEntered
{
add => Wrapped.Connect(SignalName.ChildStateEntered, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.ChildStateEntered, Callable.From(value));
}

/// <summary>
/// Called when a child state is exited.
/// </summary>
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)
{
}
Expand All @@ -37,14 +54,11 @@ private CompoundState(Node wrapped) : base(wrapped)

public new class SignalName : StateChartState.SignalName
{
/// <summary>
/// Called when a child state is entered.
/// </summary>

/// <see cref="CompoundState.ChildStateEntered"/>
public static readonly StringName ChildStateEntered = "child_state_entered";

/// <summary>
/// Called when a child state is exited.
/// </summary>
/// <see cref="CompoundState.ChildStateExited"/>
public static readonly StringName ChildStateExited = "child_state_exited";

}
Expand Down
34 changes: 20 additions & 14 deletions addons/godot_state_charts/csharp/StateChart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,26 @@ namespace GodotStateCharts
/// </summary>
public class StateChart : NodeWrapper
{
public TypeSafeSignal<Action> EventReceived { get; private set; }
/// <summary>
/// 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.
/// </summary>
public event Action<StringName> 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<Action>(Wrapped, SignalName.EventReceived);
}

/// <summary>
Expand Down Expand Up @@ -79,18 +94,9 @@ public void Step()

public class SignalName : Node.SignalName
{
/// <summary>
/// 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.
/// </summary>
/// <see cref="StateChart.EventReceived"/>
///
/// </summary>
public static readonly StringName EventReceived = "event_received";
}

Expand Down
138 changes: 88 additions & 50 deletions addons/godot_state_charts/csharp/StateChartState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,91 @@ namespace GodotStateCharts
/// </summary>
public class StateChartState : NodeWrapper
{
public TypeSafeSignal<Action> StateEntered { get; }
public TypeSafeSignal<Action> StateExited { get; }
public TypeSafeSignal<Action<StringName>> EventReceived { get; }
/// <summary>
/// Called when the state is entered.
/// </summary>
public event Action StateEntered
{
add => Wrapped.Connect(SignalName.StateEntered, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateEntered, Callable.From(value));
}

public TypeSafeSignal<Action<float>> StateProcessing { get; }
public TypeSafeSignal<Action<float>> StatePhysicsProcessing { get; }
/// <summary>
/// Called when the state is exited.
/// </summary>
public event Action StateExited
{
add => Wrapped.Connect(SignalName.StateExited, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateExited, Callable.From(value));
}

public TypeSafeSignal<Action> StateStepped { get; }
/// <summary>
/// Called when the state receives an event. Only called if the state is active.
/// </summary>
public event Action<StringName> EventReceived
{
add => Wrapped.Connect(SignalName.EventReceived, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.EventReceived, Callable.From(value));
}

/// <summary>
/// Called when the state is processing.
/// </summary>
public event Action<float> StateProcessing
{
add => Wrapped.Connect(SignalName.StateProcessing, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateProcessing, Callable.From(value));
}

public TypeSafeSignal<Action<InputEvent>> StateInput { get; }
/// <summary>
/// Called when the state is physics processing.
/// </summary>
public event Action<float> StatePhysicsProcessing
{
add => Wrapped.Connect(SignalName.StatePhysicsProcessing, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StatePhysicsProcessing, Callable.From(value));
}

public TypeSafeSignal<Action<InputEvent>> StateUnhandledInput { get; }
/// <summary>
/// Called when the state chart <code>Step</code> function is called.
/// </summary>
public event Action StateStepped
{
add => Wrapped.Connect(SignalName.StateStepped, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateStepped, Callable.From(value));
}

public TypeSafeSignal<Action<float,float>> TransitionPending { get; }

protected StateChartState(Node wrapped) : base(wrapped)
/// <summary>
/// Called when the state is receiving input.
/// </summary>
public event Action<InputEvent> StateInput
{
add => Wrapped.Connect(SignalName.StateInput, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateInput, Callable.From(value));
}

/// <summary>
/// Called when the state is receiving unhandled input.
/// </summary>
public event Action<InputEvent> StateUnhandledInput
{
StateEntered = new TypeSafeSignal<Action>(Wrapped, SignalName.StateEntered);
StateExited = new TypeSafeSignal<Action>(Wrapped, SignalName.StateExited);
EventReceived = new TypeSafeSignal<Action<StringName>>(Wrapped, SignalName.EventReceived);
StateProcessing = new TypeSafeSignal<Action<float>>(Wrapped, SignalName.StateProcessing);
StatePhysicsProcessing = new TypeSafeSignal<Action<float>>(Wrapped, SignalName.StatePhysicsProcessing);
StateStepped = new TypeSafeSignal<Action>(Wrapped, SignalName.StateStepped);
StateInput = new TypeSafeSignal<Action<InputEvent>>(Wrapped, SignalName.StateInput);
StateUnhandledInput = new TypeSafeSignal<Action<InputEvent>>(Wrapped, SignalName.StateUnhandledInput);
TransitionPending = new TypeSafeSignal<Action<float,float>>(Wrapped, SignalName.TransitionPending);
add => Wrapped.Connect(SignalName.StateUnhandledInput, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateUnhandledInput, Callable.From(value));
}

/// <summary>
/// Called every frame while a delayed transition is pending for this state.
/// Returns the initial delay and the remaining delay of the transition.
/// </summary>
public event Action<float,float> TransitionPending
{
add => Wrapped.Connect(SignalName.TransitionPending, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.TransitionPending, Callable.From(value));
}


protected StateChartState(Node wrapped) : base(wrapped) {}


/// <summary>
/// Creates a wrapper object around the given node and verifies that the node
Expand Down Expand Up @@ -66,51 +124,31 @@ public static StateChartState Of(Node state)
public class SignalName : Godot.Node.SignalName
{

/// <summary>
/// Called when the state is entered.
/// </summary>
/// <see cref="StateChartState.StateEntered"/>
public static readonly StringName StateEntered = "state_entered";

/// <summary>
/// Called when the state is exited.
/// </summary>
/// <see cref="StateChartState.StateExited"/>
public static readonly StringName StateExited = "state_exited";

/// <summary>
/// Called when the state receives an event. Only called if the state is active.
/// </summary>
/// <see cref="StateChartState.EventReceived"/>
public static readonly StringName EventReceived = "event_received";

/// <summary>
/// Called when the state is processing.
/// </summary>
/// <see cref="StateChartState.StateProcessing"/>
public static readonly StringName StateProcessing = "state_processing";

/// <summary>
/// Called when the state is physics processing.
/// </summary>

/// <see cref="StateChartState.StatePhysicsProcessing"/>
public static readonly StringName StatePhysicsProcessing = "state_physics_processing";

/// <summary>
/// Called when the state chart <code>Step</code> function is called.
/// </summary>
/// <see cref="StateChartState.StateStepped"/>
public static readonly StringName StateStepped = "state_stepped";

/// <summary>
/// Called when the state is receiving input.
/// </summary>
/// <see cref="StateChartState.StateInput"/>
public static readonly StringName StateInput = "state_input";


/// <summary>
/// Called when the state is receiving unhandled input.
/// </summary>
/// <see cref="StateChartState.StateUnhandledInput"/>
public static readonly StringName StateUnhandledInput = "state_unhandled_input";

/// <summary>
/// Called every frame while a delayed transition is pending for this state.
/// Returns the initial delay and the remaining delay of the transition.
/// </summary>
/// <see cref="StateChartState.TransitionPending"/>
public static readonly StringName TransitionPending = "transition_pending";

}
Expand Down
33 changes: 26 additions & 7 deletions addons/godot_state_charts/csharp/Transition.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
using System;

namespace GodotStateCharts
{
using Godot;

/// <summary>
/// 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.
/// </summary>
public class Transition {
public class Transition : NodeWrapper {

/// <summary>
/// Called when the transition is taken.
/// </summary>
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<Script>() is not GDScript gdScript
|| !gdScript.ResourcePath.EndsWith("transition.gd"))
{
throw new ArgumentException("Given node is not a transition.");
}
return new Transition(transition);
}


public class SignalName : Godot.Node.SignalName
{
/// <summary>
/// Called when the transition is taken.
/// </summary>
/// <see cref="Transition.Taken"/>
public static readonly StringName Taken = "taken";
}
}
Expand Down
Loading

0 comments on commit 0eb4fc8

Please sign in to comment.