Skip to content

Instantly share code, notes, and snippets.

@CarsonMcKinstry
Created August 4, 2025 15:36
Show Gist options
  • Select an option

  • Save CarsonMcKinstry/19a7c3a94ee080fae9812074177433b4 to your computer and use it in GitHub Desktop.

Select an option

Save CarsonMcKinstry/19a7c3a94ee080fae9812074177433b4 to your computer and use it in GitHub Desktop.
using System;
using Godot;
using Godot.Collections;
using ProjectTome.Utils;
namespace ProjectTome.Core.StateMachine;
[GlobalClass]
public partial class StateMachine : Node
{
[Signal]
public delegate void StateChangedEventHandler(string stateName);
private Dictionary<string, StateNode> _nodes = new();
[Export]
private StateNode? _currentState;
private bool _isTransitioning;
public string? CurrentState
{
get => _currentState?.Name;
}
public bool HasState(string stateName)
{
return _nodes.ContainsKey(stateName);
}
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
RegisterStateNodes();
GetParent().Ready += OnParentReady;
}
private void OnParentReady()
{
if (_currentState == null)
return;
_currentState.Transition += HandleTransition;
_currentState.OnEnter(new TransitionData());
_currentState.ProcessMode = ProcessModeEnum.Always;
GetParent().Ready -= OnParentReady;
}
public void TransitionTo(string stateName, TransitionData? transitionData = null)
{
if (!HasState(stateName))
{
throw GdException.Error(
$"Attempted to transition to state '{stateName}', which isn't registered to this "
);
}
var nextState = _nodes[stateName];
HandleTransition(nextState, transitionData ?? new TransitionData());
}
private void HandleTransition(StateNode nextState, TransitionData transitionData)
{
if (_isTransitioning)
{
GD.PushWarning(
$"[{GetParent().Name}]: Attempted to transition from {CurrentState} to {nextState.Name} in the middle of a transition."
);
return;
}
if (!HasState(nextState.Name))
{
throw GdException.Error(
$"Attempted to transition to state '{nextState.Name}', which isn't registered to this state machine"
);
}
_isTransitioning = true;
if (_currentState != null)
{
_currentState.ProcessMode = ProcessModeEnum.Disabled;
_currentState.Transition -= HandleTransition;
_currentState.OnExit();
}
_currentState = nextState;
_currentState.Transition += HandleTransition;
_currentState.OnEnter(transitionData);
_currentState.ProcessMode = ProcessModeEnum.Always;
EmitSignalStateChanged(CurrentState);
_isTransitioning = false;
}
private void RegisterStateNodes()
{
foreach (var child in GetChildren())
{
if (child is not StateNode node)
continue;
AddNode(node);
}
}
private void AddNode(StateNode node)
{
_nodes[node.Name] = node;
node.ProcessMode = ProcessModeEnum.Disabled;
}
}
using System;
using Godot;
using Godot.Collections;
namespace ProjectTome.Core.StateMachine;
[GlobalClass]
public partial class StateNode : Node
{
[Signal]
public delegate void TransitionEventHandler(StateNode nextNode, TransitionData transitionData);
public virtual void OnEnter(ITransitionData transitionData) { }
public virtual void OnExit() { }
protected void TransitionTo(StateNode nextNode, TransitionData? maybeTransitionData = null)
{
var transitionData = maybeTransitionData ?? new TransitionData();
Callable.From(() => EmitSignalTransition(nextNode, transitionData)).CallDeferred();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment