Skip to content

Commit

Permalink
Csharp type safe signals (#139)
Browse files Browse the repository at this point in the history
* feat: type safe signals for C#

* docs: add description for new default values feature

* feat: type safe signals for C#

* feat: provide type safe signal wrappers for C#

fixes #126, #130
  • Loading branch information
derkork authored Aug 16, 2024
1 parent 7003db4 commit 8ef2fc0
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 57 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
33 changes: 21 additions & 12 deletions addons/godot_state_charts/csharp/StateChart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ namespace GodotStateCharts
/// </summary>
public class StateChart : NodeWrapper
{
/// <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)
{
}
Expand Down Expand Up @@ -76,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
125 changes: 94 additions & 31 deletions addons/godot_state_charts/csharp/StateChartState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,91 @@ namespace GodotStateCharts
/// </summary>
public class StateChartState : NodeWrapper
{
/// <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));
}

/// <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));
}

/// <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));
}

/// <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));
}

/// <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));
}

/// <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
{
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) { }
protected StateChartState(Node wrapped) : base(wrapped) {}


/// <summary>
/// Creates a wrapper object around the given node and verifies that the node
Expand Down Expand Up @@ -41,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
83 changes: 83 additions & 0 deletions addons/godot_state_charts/csharp/TypeSafeSignal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using Godot;

namespace GodotStateCharts;

public class TypeSafeSignal<R>
{
private readonly Node _node;
private readonly StringName _signalName;

public TypeSafeSignal(Node node, StringName signalName)
{
_node = node;
_signalName = signalName;
}

public void Connect(TypeSafeReceiver<R> receiver)
{
_node.Connect(_signalName, receiver.Callable);
}

public void Disconnect(TypeSafeReceiver<R> receiver)
{
_node.Disconnect(_signalName, receiver.Callable);
}
}

public struct TypeSafeReceiver<TR>
{
internal TypeSafeReceiver(Callable callable)
{
Callable = callable;
}

public Callable Callable { get; }
}

public static class TypeSafeExtensions
{
public static void Connect(this TypeSafeSignal<Action> signal, Action action)
{
signal.Connect(new TypeSafeReceiver<Action>(Callable.From(action)));
}

public static void Disconnect(this TypeSafeSignal<Action> signal, Action action)
{
signal.Disconnect(new TypeSafeReceiver<Action>(Callable.From(action)));
}

public static void Connect<T>(this TypeSafeSignal<T> signal, Action<T> action)
{
signal.Connect(new TypeSafeReceiver<T>(Callable.From(action)));
}

public static void Disconnect<T>(this TypeSafeSignal<T> signal, Action<T> action)
{
signal.Disconnect(new TypeSafeReceiver<T>(Callable.From(action)));
}

// two args

public static void Connect<T1, T2>(this TypeSafeSignal<Action<T1, T2>> signal, Action<T1, T2> action)
{
signal.Connect(new TypeSafeReceiver<Action<T1, T2>>(Callable.From(action)));
}

public static void Disconnect<T1, T2>(this TypeSafeSignal<Action<T1, T2>> signal, Action<T1, T2> action)
{
signal.Disconnect(new TypeSafeReceiver<Action<T1, T2>>(Callable.From(action)));
}

// three args

public static void Connect<T1, T2, T3>(this TypeSafeSignal<Action<T1, T2, T3>> signal, Action<T1, T2, T3> action)
{
signal.Connect(new TypeSafeReceiver<Action<T1, T2, T3>>(Callable.From(action)));
}

public static void Disconnect<T1, T2, T3>(this TypeSafeSignal<Action<T1, T2, T3>> signal, Action<T1, T2, T3> action)
{
signal.Disconnect(new TypeSafeReceiver<Action<T1, T2, T3>>(Callable.From(action)));
}
}

0 comments on commit 8ef2fc0

Please sign in to comment.