GithubHelp home page GithubHelp logo

godot-statecharts's People

Contributors

alitnk avatar derkork avatar folt-a avatar jtakakura avatar mechpensketch avatar uzkbwza avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

godot-statecharts's Issues

Any tips to get around refactoring and deleting links when linking state signal directly from editor?

I've been using statecharts extensively recently. As the scale of connecting to state signals directly in the editor increases, refactoring and deleting links have become a significant challenge. The pain points are as follows:

When the name of an already-connected node changes, the associated function name does not update. For example, in the platformer example, a node initially named 'jump enabled' might have a function _on_jump_enabled_state_physics_processing. If the node's name is changed to 'boost enabled,' the function name remains the same, even though everything still functions correctly.

If the function code is deleted or cut-and-pasted to another location within the same script, a record remains under the 'Signals' tab pointing to the original line number. Double-clicking this record takes you to the incorrect line.

When connecting to a state signal, there is no option to choose the location for the new code block. It is always appended after the last function in the script, which can make the code messy if multiple connections are made.

For the sake of long-term code cleanliness, I find myself needing to delete the code, remove the connection in the 'Signals' tab, reconnect, and accept that the new code will be added to the bottom of the script.

I believe these issues are more related to Godot than to statecharts. However, I'm curious if you have any workarounds to mitigate these challenges. As more people start using statecharts, I believe these refactoring issues could become a serious blocker. Thank you!

AntHill demo calling "initialized" too early

Hi,

Thanks for the great addon to Godot. I am a newbie to Godot so please excuse any mistakes below. I was trying to understand the AddOn better by running the AntHill demo in Godot v4.1.1(stable) and the ants never switched from "Initializing" to "Seeking food" state. I used the debugger (nice tool) to see that the signal was never acted on. So I added a timer to the Ant scene, with a wait time of 3 seconds. When it timed out, I sent the signal again (via timer.timeout.connect(func (): state_chart.send_event("initialized")), and it successfully started the demo.

I'm sure there's a more elegant way of doing this, but it seems that the signal is sent too early?

Allow the sidebar to be moved to the right

hey, really enjoying the plugin! i have a (hopefully ๐Ÿ˜…) quick usability suggestion. in my godot setup, the inspector is on the right side dock, so being able to have the State helper sidebar on the right closer to the inspector would be nice, since it seems to be intended that it's close to the inspector.

AnimationPlayerState animation_name is ignored

addons/godot_state_charts/animation_player_state.gd

animation_name property is never used.

I expected this comment

## The name of the animation that should be played when this state is entered.
## When this is empty, the name of this state will be used.
@export var animation_name: StringName = ""

Request: Time in State, Time Elapsed

Greetings! I've been getting a lot of use out of this plugin and I'm chuffed it exists. Back when my project was on Unity, I used a class-based state machine which had one thing I can't seem to find here in any way -- a way to measure how long a state has been active (or at least, this is not available in a way to which I am accustomed, as I am brand new to Godot).

Summary

In my old code, you could find the time in state by doing the following

//Tracks the starting time of entering a state.
protected float _stateStartTime; 

//Tracks how long an object has been in a given state.
protected float _timeInState { get { return Time.time - _stateStartTime; } }

And then for every state, on entering it, update the _stateStartTime, then you can measure the time spent in the state using _timeInState. Right now, I have to ensure my animations are correctly timed to do this, and delayed transitions are not as useful as I thought if the send_event function is called each frame.

The other alternative has been to use Godot timers, which have their own issues whether one is creating a new node for every single instance of a thing that needs doing, updating an existing one to handle most operations, or creating timers in-line, but those cannot be easily accessed once created.

Implementation?

I tried to implement this solution myself by modifying the plugin, but realized that one can only access the State Chart itself, not individual states where we'd naturally be recording time in state and state start time. For the moment, I just make this on the object using the state machine, but this requires a lot of boilerplate for each given state.

## The starting time of entering a state.
var _state_start_time : int = 0

## How long a state has been active. Returns the value in seconds.
func _state_time_elapsed() -> int:
	return (Time.get_ticks_msec() - _state_start_time) * 0.001

And then, on a given state's _state_enter(), simply set _state_start_time = Time.get_ticks_msec().

Conclusion

I know it's in the spirit of your design to avoid polling the state too much to keep things clean and streamlined, but having access to time within a given state is so very important to the use of a state machine. In my case, being able to reference time elapsed in a state to facilitate transitions and control the finer feel of my game is something I would consider an essential feature.

If there exists a better way to access this as-is that I've missed, please let me know, and thanks again for your work on this plugin!

Strange lack of 'state transitions'

I don't even know whats going on here. Simply put I call '_state_chart.send_event("air")' and this happens:

  • Doesn't transition?
  • stays on the defaulted 'grounded' state
    _does on_grounded_event_received(event) suppose to be receiving event 'air' nonstop?
    image

Node tree:
image

I'm not crazy right? this is all you need to change states:

@onready var _state_chart = $StateChart as StateChart

func _on_grounded_state_physics_processing(delta):
	# temp gravity
	velocity.y += -2.5

	if self.is_on_floor() and jump_queued:
		velocity.y += 20
		_state_chart.send_event("air")

# This is just to show the complete picture:
func _on_grounded_state_input(event):
	pass

# Ariel Movement ๐Ÿช‚
func _on_air_state_entered():
	print("hello???")


func _on_air_state_physics_processing(delta):
	move_and_slide()

The signals are connected and just double check right now. Gonna start reinstalling everything including godot cause this is a silly problem tbh.

StateCharts platformer demo animation bug

I am not sure if it is due to my version of GoDot used. I am using GoDot 4.1.1.

When I open the platformer demo that comes with the tool and try to run it, everything seems to work fine except when you do a double jump. The character is stuck in the spinning animation but the state in the debug box seems to be correctly displaying the states.

StateChartDebugger drops to 5 FPS

After upgrade to 0.4.0 my Player StateCharts is not working properly (bug in my code), so I tried to debug it but StateChartDebugger drops my project FPS to 5 from 60 :D

I'm sure it worked fine in 0.3.1.

Add Beehave Style Integrated Debugger UI

The StateChartDebugger Node is useful for debugging a single statechart at a time but feels less than ideal when trying to debug startcharts for multiple scenes at once as it seems to require resizing each debug control node so they don't overlap for each debugger you want to see at once.

BeeHave has a very nice debug interface that is added as a tab in the Godot Debugger UI that lets you view each of the active behavior trees in the current scene. godot-statecharts could do something similar by taking the existing StateChartDebugger UI and auto registering each statechart into a Godot debugger interface tab with a list to pick which statechart to view the debug ui for. BeeHave adds an auto-loaded global script to handle the debug registration in the root behavior tree node.

Great library. I felt like I could finally get a handle on how to use statecharts/FSMs with the node based approach compared to relatively cumbersome (for non trivial charts) libraries like XState for javascript.

Transition class_name conflicts easily

Hi there!

I have noticed that the class_name of Transition, which you use, conflicts very easily with lots of other projects. I was wondering if it could be changed to something like StateChartTransition? I'm happy to open a PR for this, let me know if you'd like me to change that. I only noticed because it conflicts with my public game template, which is in pretty widespread use, so I'll be updating my template as well (we use Transition for scene transitions as an autoload, so that's where the conflict comes in, and Godot's error reporting on this is...poor, to say the least), but in general Transition seems to be used regularly across several projects, not just our two, meaning it's going to come up more often I should think, if StateCharts becomes very popular.

Process callbacks should not be disabled in State nodes

Currently, in the State.gd script, the process callbacks for the script's node are disabled to implement the functionality of the State process events being signalled every process and physics frame:

	func _toggle_processing(active: bool):
	set_process(active and _has_connections(state_processing))
	set_physics_process(active and _has_connections(state_physics_processing))
	set_process_input(active and _has_connections(state_input))
	set_process_unhandled_input(active and _has_connections(state_unhandled_input))

This is not ideal, since this makes it very difficult to expand on the functionality of the State class AND its sub-classes in the future by adding new logic that needs to use the process callbacks. The same goes for the input callbacks which might be needed in the future, but are currently also being disabled.

Instead, local variables should be used to check whether the signal state_processing(delta:float) and signal state_physics_processing(delta:float) signals should be emitted:

var should_emit_physics_process_signal: bool = true
var should_emit_process_signal: bool = true
var should_handle_input: bool = true
var should_handle_unhandled_input: bool = true

func _toggle_processing(active: bool):
	should_emit_physics_process_signal = active and _has_connections(state_physics_processing)
	should_emit_process_signal = active and _has_connections(state_processing)
	should_handle_input = active and _has_connections(state_input)
	should_handle_unhandled_input = active and _has_connections(state_unhandled_input)

OnReady vars null on initial enter state

Using vars that are set with @onready (or in _ready()) are still unset when the Initial State is first Entered.
Only workaround I have found, for nodes use $Node/Path or get_node(). For dynamic vars, would need to be hard-coded.

I have not found an elegant way around this to "fix", and there may not be. A note or warning may be nice about the load order of when in runs the initial state enter signal.

Events with additional payload data

I'm trying out this library for enemy behavior. The problem I ran into is that it's hard to keep track of data attached to my incoming events. I have a damage event handler with the signature:

func _on_takes_damage(hurtbox:Area3D, damage:HitDamage):

My impulse is to pass it into the state chart, and then use event handlers to change behavior based on the state, but this currently isn't possible due to send_event only taking a single argument.

Would it make sense to add a payload argument?

I'm imagining:

# Signal from the hurtbox
func _on_gets_hit(damage:HitDamage, player):
	state_chart.send_event('take_damage', {
		'damage': damage,
		'player': player,
	})

# From the statechart:
func _on_calm_event_received(event, payload):
	match event:
		'take_damage':
			apply_damage(payload['damage'].raw_damage)
			print('Player %s is a jerk' % payload['player'] )

func _on_enraged_event_received(event, payload):
	match event:
		'take_damage':
			var reduced_damage = payload['damage'].raw_damage * 0.5
			apply_damage(reduced_damage)

This would also allow processing of events like Area2D body_entered(body) through the statechart while retaining information about the triggering body.

event_received emits same event every time?

I'm trying to implement an input buffer in my game. This will allow the player to buffer 'inputs' during an attack animation. In this case however they are really buffering SendEvent() function calls.

In my game there is an intermediary script called StateChartQueue.cs which forwards events to the StateChart node. At any point I can disable the forwarding of events and instead store them into a buffer. This buffer can later be 'flushed' to the StateChart node.

The event_received signal, as emitted by the StateChart node, is used in StateChartQueue.cs to validate the order of events flushed and received. This is so I can truly control the order in which the events are processed when flushing to the StateChart. What I'm trying:

  1. eventList = { "apple", "orange" }
  2. SendEvent("apple")
  3. OnEventRecieved(event)
  4. If event == "apple"
  5. SendEvent("orange")
Relevant code
  partial class DominoRunner : Node
  {
      readonly string[] events;
      readonly StateChart stateChart;

      string topplingEvent;
      int dominoIndex;

      internal DominoRunner(StateChart stateChart, string[] events)
      {
          this.events = events;
          this.stateChart = stateChart;
      }

      void OnEventReceived(StringName @event)
      {
          GD.Print($"Received {@event}.");
          if (@event != topplingEvent)
          {
              GD.Print($"{@event} was not {topplingEvent}.");
              //return;
          }

          dominoIndex++;

          if (dominoIndex >= events.Length)
          {
              GD.Print("Finished domino.");
              QueueFree();
              return;
          }

          topplingEvent = events[dominoIndex];

          GD.Print($"Toppling {topplingEvent}, index {dominoIndex}.");
          stateChart.SendEvent(topplingEvent);
      }

      internal void Topple()
      {
          dominoIndex = 0;
          topplingEvent = events[dominoIndex];

          foreach (string str in events)
          {
              GD.Print($"{str} in domino list.");
          }

          stateChart.Connect("event_received", new Callable(this, nameof(OnEventReceived)));
          GD.Print($"Toppling initial event {topplingEvent}, index {dominoIndex}.");
          stateChart.SendEvent(topplingEvent);
      }

      static string[] SortEventsByWeight(string[] events)
      {
          Dictionary<string, int> weights = new()
      {
          { "usable", 1 }
      };

          return events.OrderBy(item => weights.ContainsKey(item) ? weights[item] : 0).ToArray();
      }
  }
Full StateChartQueue.cs script
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using GodotStateCharts;
using Grindle;

public partial class StateChartQueue : Node2D
{
  readonly Dictionary<StateChart, bool> recording = new();
  readonly Dictionary<StateChart, List<string>> events = new();
  readonly Dictionary<StateChart, string[]> eventBlackLists = new();
  bool flushing;

  readonly Godot.Collections.Array<string> baseEventBlacklist = new()
  {
      "restorePlayer"
  };

  public static void SetExpressionProperty(StateChart stateChart, string name, Variant value)
  {
      stateChart.SetExpressionProperty(name, value);
  }

  public void SendEvent(StateChart stateChart, string @event)
  {
      if (!Methods.IsValid(stateChart))
      {
          throw new ArgumentException("StateChart is null.");
      }

      if (!recording.ContainsKey(stateChart))
      {
          GD.Print($"{stateChart.Wrapped} not found in keys, creating keys.");
          recording.Add(stateChart, false);
          events.Add(stateChart, new());
          eventBlackLists.Add(stateChart, Array.Empty<string>());
      }

      if (flushing)
      {
          GD.Print("Cannot send event during flush.");
          return;
      }

      if (recording[stateChart])
      {
          if (eventBlackLists[stateChart].Contains(@event))
          {
              GD.Print($"Bypassing record for blacklisted event {@event}.");
              stateChart.SendEvent(@event);
              return;
          }

          GD.Print($"Recording {@event} to key {stateChart.Wrapped}.");
          events[stateChart].Add(@event);
          return;
      }

      GD.Print($"Not recording on {stateChart.Wrapped}, sending event {@event}.");
      stateChart.SendEvent(@event);

  }

  public void Flush(StateChart stateChart)
  {
      if (!Methods.IsValid(stateChart))
      {
          throw new ArgumentException("Given StateChart is null.");
      }

      if (!recording.ContainsKey(stateChart))
      {
          throw new Exception($"Cannot flush unregistered {stateChart.Wrapped}.");
      }

      recording[stateChart] = false;

      if (events[stateChart].Count == 0)
      {
          GD.Print($"Nothing to flush for key {stateChart.Wrapped}.");
          return;
      }

      flushing = true;

      DominoRunner dominoRunner = new(stateChart, events[stateChart].ToArray());
      dominoRunner.Topple();

      events[stateChart].Clear();

      flushing = false;
  }

  public void Record(StateChart stateChart, Godot.Collections.Array<string> eventBlackList)
  {
      if (!Methods.IsValid(stateChart))
      {
          throw new ArgumentException("StateChart is null.");
      }

      foreach (string str in eventBlackList)
      {
          GD.Print($"{str} is in received blacklist.");
      }

      Godot.Collections.Array<string> _eventBlackList = new();
      _eventBlackList.AddRange(baseEventBlacklist);
      _eventBlackList.AddRange(eventBlackList);

      foreach (string str in _eventBlackList)
      {
          GD.Print($"{str} is in blacklist.");
      }

      if (!recording.ContainsKey(stateChart))
      {
          GD.Print($"{stateChart.Wrapped} not found in keys, adding.");
          recording.Add(stateChart, true);
          events.Add(stateChart, new());
          eventBlackLists.Add(stateChart, _eventBlackList.ToArray());
          return;
      }

      if (recording[stateChart])
      {
          GD.PrintErr($"Already recording {stateChart.Wrapped}.");
          return;
      }

      GD.Print($"{stateChart.Wrapped} now recording.");

      recording[stateChart] = true;
      events[stateChart].Clear();
      eventBlackLists[stateChart] = _eventBlackList.ToArray();
  }

  partial class DominoRunner : Node
  {
      readonly string[] events;
      readonly StateChart stateChart;

      string topplingEvent;
      int dominoIndex;

      internal DominoRunner(StateChart stateChart, string[] events)
      {
          this.events = events;
          this.stateChart = stateChart;
      }

      void OnEventReceived(StringName @event)
      {
          GD.Print($"Received {@event}.");
          if (@event != topplingEvent)
          {
              GD.Print($"{@event} was not {topplingEvent}.");
              //return;
          }

          dominoIndex++;

          if (dominoIndex >= events.Length)
          {
              GD.Print("Finished domino.");
              QueueFree();
              return;
          }

          topplingEvent = events[dominoIndex];

          GD.Print($"Toppling {topplingEvent}, index {dominoIndex}.");
          stateChart.Wrapped.CallDeferred("send_event", topplingEvent);
      }

      internal void Topple()
      {
          dominoIndex = 0;
          topplingEvent = events[dominoIndex];

          foreach (string str in events)
          {
              GD.Print($"{str} in domino list.");
          }

          stateChart.Connect("event_received", new Callable(this, nameof(OnEventReceived)));
          GD.Print($"Toppling initial event {topplingEvent}, index {dominoIndex}.");
          stateChart.SendEvent(topplingEvent);
      }

      static string[] SortEventsByWeight(string[] events)
      {
          Dictionary<string, int> weights = new()
      {
          { "usable", 1 }
      };

          return events.OrderBy(item => weights.ContainsKey(item) ? weights[item] : 0).ToArray();
      }
  }
}

The problem is that OnEventReceived(StringName @event) is being passed the same event string every time it is called, which is mainly what I don't understand. Because of this, I just can't use the event to the effect that I want. Here is a log showing this:

Relevant logs
Not recording on <Node#43671094576>, sending event usable.
restorePlayer is in blacklist.
<Node#43671094576> now recording.
Recording jog to key <Node#43671094576>.
Recording moveUp to key <Node#43671094576>.
Recording stop to key <Node#43671094576>.
Recording jog to key <Node#43671094576>.
Recording moveRight to key <Node#43671094576>.
Recording stop to key <Node#43671094576>.
Bypassing record for blacklisted event restorePlayer.
jog in domino list.
moveUp in domino list.
stop in domino list.
jog in domino list.
moveRight in domino list.
stop in domino list.
Toppling initial event jog, index 0.
Received jog.
Toppling moveUp, index 1.
Received jog.
jog was not moveUp.
Toppling stop, index 2.
Received jog.
jog was not stop.
Toppling jog, index 3.
Received jog.
Toppling moveRight, index 4.
Received jog.
jog was not moveRight.
Toppling stop, index 5.
Received jog.
jog was not stop.
Finished domino.

As you can hopefully see, the StateChart node is passing the first event I sent to it for this entire 'loop'. There is still so much I don't know about game engines and programming in general, so maybe I'm just dumb. Any idea what the issue might be?

Info: I'm executing Record() and Flush() from an AnimationPlayer.

expression_guard "Expression execute error"

For some reason I get the following errors when using an ExpressionGuard:

E 0:00:02:0229   expression_guard.gd:35 @ is_satisfied(): self can't be used because instance is null (not passed)
  <C++ Error>    Condition "p_show_error" is true. Returning: Variant()
  <C++ Source>   core/math/expression.cpp:1507 @ execute()
  <Stack Trace>  expression_guard.gd:35 @ is_satisfied()
                 transition.gd:51 @ evaluate_guard()
                 state.gd:97 @ _state_enter()
                 compound_state.gd:52 @ _state_enter()
                 compound_state.gd:52 @ _state_enter()
                 parallel_state.gd:68 @ _state_enter()
E 0:00:02:0229   expression_guard.gd:37 @ is_satisfied(): Expression execute error: self can't be used because instance is null (not passed) for expression: lastMovement == "moveUp"
  <C++ Source>   ./core/variant/variant_utility.cpp:905 @ push_error()
  <Stack Trace>  expression_guard.gd:37 @ is_satisfied()
                 transition.gd:51 @ evaluate_guard()
                 state.gd:97 @ _state_enter()
                 compound_state.gd:52 @ _state_enter()
                 compound_state.gd:52 @ _state_enter()
                 parallel_state.gd:68 @ _state_enter()

Regardless, the statechart still functions as expected. There is just a little bloat in the Debugger. This is how I'm calling set_expression_property in my C# script:

void OnPlayerStop(Movement movement)
{
    string value = movement switch
    {
        Movement.Up => "moveUp",
        Movement.Down => "moveDown",
        Movement.Left => "moveLeft",
        Movement.Right => "moveRight",
        _ => "moveDown"
    };

    stateChart.Call("set_expression_property", "lastMovement", value);
    stateChart.Call("send_event", "stop");
}

My node tree looks like this:
image
ToIdleUp properties:
image

support for _integrate_forces?

From what I've been able to tell this state chart implementation doesn't appear to work with _integrate_forces? If I'm understanding that correctly, is that something that could be added to the State class?

_integrate_forces is used primarily when controlling StaticBody2D objects in kinematic mode.

Properly polling input for _on_[state]_state_input signals

I recorded a quick Youtube video that walks through the issue. More info below.

When I first installed this plugin, I made the mistake of putting all of my input polling in the physics_processing signals, which is not ideal for obvious reasons.

I've been going through the process (pun intended) of moving every oneshot-style input to a state's given _input signal, and any other inputs that need to be polled every frame into _process. I'm having issues where the state machine is performing its transitions correctly as it did before, but the behavior is now broken, with states entering and exiting so quickly that an animation is never fired, and states are re-entered almost immediately.


EXAMPLE

The Roll State

I have a state, Roll, where the player rolls. The animation tree is set to return to the Idle animation once the roll finishes, which is how we check to see if we should return to Idle:

func _on_roll_state_entered():
	_timer_roll.start()
	_animations.travel("Roll")
	print("Roll state entered.")

func _on_roll_state_physics_processing(delta):
	velocity = _dir_to_vector(direction) * roll_speed
	if _animations.get_current_node() != "Roll":
		_state_chart.send_event("idling")
		print("roll done")

And the transition:
image


The Idle State

_on_idle_state_input(event)

func _on_idle_state_input(event):	
	if event.is_action_pressed("Evade") && _is_off_cooldown(_timer_roll):
		_state_chart.send_event("rolling")
		print("we roll now")

RESULT
Fails. State processes transition, goes to Roll for one frame, returns to Idle immediately before an animation fires.

_on_idle_state_physics_processing(delta)

	if Input.is_action_just_pressed("Evade") && _is_off_cooldown(_timer_roll):
		_state_chart.send_event("rolling")
		print("we roll now")	

RESULT
Works, but is physics_processing, so user input is at risk of being dropped.

_on_idle_state_processing(delta)

	if Input.is_action_just_pressed("Evade") && _is_off_cooldown(_timer_roll):
		_state_chart.send_event("rolling")
		print("we roll now")	

RESULT
Fails. State processes transition, goes to Roll for one frame, returns to Idle immediately before an animation fires.


Processing input outside of FSM

What is strange is that I get a different result when I poll inputs outside of the state charts.

_input(event)

func  _input(event):	
	if event.is_action_pressed("Evade") && _is_off_cooldown(_timer_roll):
		_state_chart.send_event("rolling")
		print("we roll now")

RESULT
Fails. State processes transition, goes to Roll for one frame, returns to Idle immediately before an animation fires.

_process(delta)

	if Input.is_action_just_pressed("Evade") && _is_off_cooldown(_timer_roll):
		_state_chart.send_event("rolling")
		print("we roll now")	

RESULT
Works. State processing and behavior occurs as intended and expected.

I won't post about physics_processing, since that works in the state chart and isn't the solution I want anyway.


CONCLUSION

I'm not quite sure what is going on. I have a Jump state that also works just fine. I wanted to say that

	if _animations.get_current_node() != "Roll":
		_state_chart.send_event("idling")

was the culprit, because my Jump animation immediately sets a boolean which that state's physics_processing checks every frame of the jump (if you're grounded, go back to idle). And that works just fine in state_input. But this seems less likely when I can use the same logic in a generic _input function and it runs as expected.

I'm pretty lost at this point, and would love some help. Thanks in advance.

Add Transition Properties: CancelEvent, CancelEventGuard, CancelCondition and EndGuard

Hello there,

I think is necessary to add a way to cancel transitions that should not be continued.

Here is an example scenario: You have a sleeping enemy in your game, you want them to wake up 3 seconds after the player entered their detection radius. All is well when the player approaches them, the enemy transitions from a sleeping state to an active state after 3 seconds. However, if the player leaves the detection radius before the 3 seconds elapse, you would like the enemy to remain in a sleeping state.

Currently, to solve this problem you can create another transition on the enemy that instantly transitions back to its own sleeping state when the player exits the detection radius. But this is a hack, and is not ideal. Instead, there should be a way to add a check that runs every physics frame to see if the transition should be continued. Also, there should be a way to cancel the transition based on certain events that happen, and that event listener should also be able to have a guard property.

Locally I have already edited transitions in my project so they have cancel events and conditions, and they work nicely and make state management a lot easier:
image

I think these would add substantial value to the plugin.

Kind regards

Icons are way too big

Icons are way too big consider using the same size of icons that the rest of the editor uses

AnimationFinished Guard

I know I asked about this earlier but it would be nice to have a AnimationPlayerFinishedGuard and a AnimationTreeFinishedGuard

"Could not find type "Transition" in current scope" error

My project is primarily a C# project, but is currently using Godot State Charts as an AI solution. However, this appears to create an issue where sometimes the project freezes on launch and produces a parser error: "Could not find type "Transition" in current scope", pointing to res://addons/godot_state_charts/state_chart.gd - line 13. Luckily, restarting the game always fixes this issue and manually building the project before launching the game usually prevents the issue from occurring, but it is still a significant workflow disruption.

Some solutions that have already been tried include deleting and reimporting the project, updating Godot versions (I am currently using the Godot 4.2 release), and clearing cache files that may point to a broken file link.

You can use expression guards as complex transition conditions

If you never use the event field in transitions and always leave it blank, you can instead just use expression guards on your transitions and use these as conditional statements for the transitions.

The only additional requirement for this work around is to emit a blank event [duration of your choosing] from a managing object and it works very cleanly to allow complex conditions to work in your transitions.

Is there any reason you would be against having this as a fully built in supported feature instead of work arounds?
Or if this was the intention then it works great!

HistoryState doesn't work

Please check HistoryState does not work, I added it as a child node to CompoundState, but when HistoryState transitions to history and _state_restore is not called

Transitioning to the root states throw exception

When a transition is set to the root compound state ( the child of the StateChart node ), the path resolution throw at compound_state.gd at lign 188 : _handle_transition(...)::get_parent()._handle_transition(transition, source) :

image

It seems like returning to this statenode tries to get the handle_transition of the state chart.

My reasoning was that I was trying to, for a lack of better word, return to the normal state, which is falling. I tried to transition to the root compound state since I have my logic that work during initialization. It's confusing since the root compound state has a field named initial state so I though it would work.

The workaround for now is that my transition return to the falling state instead of the root node.

Possible solution :
Add an handle_transition function in state_chart that let the one and only one child resolve it, as if it was called by _ready

Using state charts for stepping in/out of menus

I'm not sure if my "issue" here is with this plugin or with state charts, but here goes.

I'm in the very early stages of doing a tile-based battler, similar to Fire Emblem or FFTA. During a single turn of combat, the menu works like this:

MoveState
- on spacebar, accept position and go to ActionMenu

ActionMenuState
- on clicking Attack, proceed to TargetSelect
- on clicking Spells, proceed to TargetSelect
- on escape, revert to MoveState
TargetSelectState
- on selecting an attack direction, proceed to TargetConfirm
- on escape, revert to ActionMenu
TargetConfirm
- on confirming, revert to MoveState
- on escape, remove everything from the stack and jump back to MoveState

I had previously set up my menu structure as a fairly simple FSM with an undo stack, so that I could keep menus onscreen (similar to something like the original Pokemon games, where successive menus appear on top of previous ones). A given menu's exit() method only got called if the undo was called, as otherwise I wanted to keep them all visible.

I switched my previous implementation to StateCharts instead. In the new implementation, the latter three states are grouped under a parent CompoundState, as I initially wanted all their "Cancel on escape" transitions to move to a HistoryState that never worked (it always went back to its default, not to the previous state).

HistoryState working or no, the problem I'm having now is that while there's only one state_exited() signal, all the attack-selection menus have two ways of being "exited:" moving to the next state via spacebar, or reverting to a previous state with escape. In the first case, I want to keep the exiting menu onscreen but disable its inputs, and in the other case I want to hide the menu entirely.

It occurs to me that to make this work, the state would need to know the transition that caused it, but that feels like it's beyond the scope of a state chart. That, or every menu would need two states somehow, but that feels like it's inviting state explosion.

Any thoughts? I know this example is fairly simple, but I'd like to be able to use this for more complicated menus later on.

add buffer size to state output in debugger

when the debugger runs for a long time it slows down the game as it endlessly adds text to the output. it should have some configurable buffer size, e.g 300 lines and automatically clear the oldest lines.

help - how to check if a StateChart is in a specific state?

hello, love your state machine asset, but how to check if a StateChart is in a specific state in code?

for example:
if body == player and player.state_chart.current_state == "idle":

i tried looking at the manual, but im still pretty novice lol
any info much appreciated!

Adding actions to transitions

The State Charts (format?) provides for actions being placed on transitions, and I think this might be a useful addition to the addon. For example, I'm working on a project with walljumping, which has different behavior from a regular jump. To model this, I've added additional "jump" states with associated entry actions so their execution can be handled by the state chart- but these states serve no other purpose, don't fit with my chart's conventions, and they aren't really states so much as actions that should happen alongside a transition.

Perhaps transitions should provide signals that emit when the transition occurs?

GUI editor support, add many state nodes quickly

Hi.

Thank you for this wonderful addon!

Are you planning to add an editor GUI Editor support that creates many State&Transition nodes?

I want to create State&Transition nodes quickly because complex state management requires many nodes.

I have created and used a feature like this editor addon codes.

https://github.com/folt-a/godot-statecharts/tree/editor_add_node_buttons

1647235403542597633-20230415_224819-vid1.mp4

1647224978197319681-img1

1647224978197319681-img2

if you are already planning to add a similar feature, please ignore this Issue and close it!

Double _on_enter() due to compound_state

It took me a while to reproduce but I found out that some architecture can trigger on_enter and on_exit twice.

Small explanation of my nodes :
I have a character that move in the Move state, then once I click it goes into Jump state and immediatly transition to a Falling state.
I've connected the on_enter() function of the Move and Falling state to a function that print respectively Move and Falling enter with the timestamp.

With this setup :
image
Produce this result :
image

However this architecture :
image
Airborne also print its name on enter
Produce this result :
image

During transition, it seems that compoundstate trigger the on_enter() and on_exit of his children if it's the target node. I've looked into the reference statechart.io website to see if it's an intended behavior, but didn't find any. I believe this is unintended behavior.

However I'm not familiar enough with the code to know why the root compoundstate isn't affected.

Thank you

Port to C#

Are there any plans to port this to C# ever?

Wrong variable type declaration, which caused errors on clicking state nodes.

Problems

The newly added GUI editor is good, but there're still some issues on it.
When I click any state nodes, the editor push errors about
"res://addons/godot_state_charts/utilities/editor_sidebar.gd:42 - Cannot call method 'get_children' on a null value"

image

Solution

this problem is caused by wrong variable type declaration in res://addons/godot_state_charts/utilities/editor_sidebar.gd , line 14,
change:

@onready var _add_grid_container:GridContainer = %AddGridContainer

to

@onready var _add_grid_container:HFlowContainer = %AddGridContainer

would be fine.
image

0.4.1

Maximum Lines property in StateChartDebugger is not working.

Very minor problem but wanted to let you know :D
I also rly like new Settings tab.

How to handle the Guard dependency of parallel state?

Suppose parallel state A and B, they recive "event_a" and "event_b" which both bind to a same godot action (like a click), if i only want to transit from B, how should I do๏ผŸ

In my real case, event_a is always called earlier than event_b, which make a StateIsActive guard on state A watching state B is useless, because transition guard of state A can't see whether B has changed because it always evaluate it before event_b send to B. Do we need some code to sync the state?

Provide signals for `_input` and `_unhandled_input`

If possible, I think that states should provide signals for _input and _unhandled_input, similar to how there are signals for _process and _physics_process. This change would allow for slightly more optimal code since the input functions only fire whenever there's a change in input, whereas the process functions fires every frame.

Currently, my workaround for this is to, in my player's _unhandled_input function, emit a state event (e.g., "interact") and then have a signal handler, which is connected to the event_received for all relevant states, handle the event appropriately. This works fine, but is a bit hacky and you do lose all information about the input event itself.

P.S. I've only just started using godot-statecharts and it has already been so useful for me! Thanks a lot for the awesome plugin!

Tips on many Movement based states for a Player scene?

Hi! First off, I just wanted to say that I love this plugin for Godot! It's been incredibly easy to work with so far, and everything feels very intuitive.

I've just started getting into Godot, and having come from a more conventional software developer background, am having trouble wrapping my head around the "right" way to accomplish composition using state charts for states that require a decent amount of logic to be ran on process/enter/exit/etc.

In some examples that I've seen, it seems that the main way to interact with StateChart states is to have other nodes listen to signals being emitted by each state and processed in a main script. For a player scene, with lots of complex movement states, this method means that I either have to have 1000+ LOC in a single file, or create bespoke nodes/scripts for handling each one (e.g. a Running state would need a Running.gd script that handles the enter/exit/processing of that state. Or more generally, a PlayerMovement.gd script that contains all movement stuff).

I'm leaning towards creating scripts that just broadly organize the logic in the player scene, but with lots of cross-cutting concerns like parallel states that also affect movement, I was curious if there was a better way to accomplish this with StateCharts?

Apologies if this is a silly question, just looking for any tips if you have any!

Change Delay Seconds from transition programmatically

Hi!

I have a very simple chart with 3 states, and I just want to move from one state to another (randomly) after some delay. Which means I should just use the "Delay Seconds" and a guard expression that checks if a random number is the correct one, which will tell the next state.

The problem I have is that I want that delay seconds to change randomly also, so every time I change states, I want this delay to be different to give some organic feeling.

Is there a way to do this? or should I use an external timer and then call the event to change state? Thanks!

state chart : Stack overflow infinite recursion

hello, I have tried the state charts addon (which is a very cool addon by the way) but for some reason when I run the game, it gives me a "Stack overflow infinite recursion" error message and the game does not run, This is without attaching any script.

image

I hope for this problem to be solved however this is a pretty good package

Stack overflow (stack size: 1024). Check for infinite recursion in your script.

Setting up a chart like the one in the demo video throws this error and it points to this code (state.gd / 281):

func _toggle_processing(active:bool):
    set_process(active and _has_connections(state_processing))
    set_physics_process(active and _has_connections(state_physics_processing))
    set_process_input(active and _has_connections(state_input))
    set_process_unhandled_input(active and _has_connections(state_unhandled_input))

Disconnecting all signals does nothing. Just the existence of the thing in my scene seems to be making it rather cranky.

Unsure if related: it doesn't always seem to recognize when I have set a default state or when a node has a child and it will still give me a little warning that says I need to select a state.

Make a release workflow

Right now the release is done manually, which is not too tedious but automating stuff is better. The release process should ideally:

  • be triggered through a workflow on tagging a git commit with vX.Y.Z
  • enter the new version into the plugin config file
  • create a GitHub release
  • submit the new version to the Godot Asset Library

Can I force the state machine into some state? For network syncs

Hi. I'm using Godot 4 high-level networking and I got a scene that uses State Charts. When a client connects to a running game session, I want the state of that scene to "jump" or be "forced into" the state that's currently on the server. Is that possible with the current API?

Thanks.

Add a way to trigger transitions via other node's signals

It be nice to be able to "trigger" transitions by connecting them to signals.

For example, connect NavigationAgent2D navigation_finished() to toIdle transition of the Run state.

At the moment we can workaround this by adding a function to a proxy that calls send_event with the appropiate event string.

What are your 5 cents on this?

0 delay transitions should happen on the same frame

Even though I'm loving this addon so far, I recently came to the sad realization that 0-second transitions do not happen on the same frame.

This is a big problem depending on how your state machine is structured. Immediate transitions now take a variable amount of frames depending on the amount of state jumps that happen, while also making it hard to perform logic after all transitions are finished.

Is there a workaround for this or is this in your plans?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.