derkork / godot-statecharts

A state charts extension for Godot 4
MIT License
734 stars 35 forks source link

Request: Time in State, Time Elapsed #46

Closed Phanterm closed 10 months ago

Phanterm commented 10 months ago

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!

BadBoyGodot commented 10 months ago

Hi Phanterm,I'm not the author of the plugin, but I found your issue really interesting. I've faced similar challenges when working with state machines.Have you considered breaking down your single state into multiple sub-states to track the time elapsed more precisely? By doing so, each sub-state could have its own lifecycle, making it easier to manage time-sensitive logic. This approach might reduce the need for boilerplate code and align more closely with the principles of state machine design.Of course, I understand the simplicity of wanting to track time within a single state, especially for straightforward logic. I was just wondering if breaking it down into sub-states might offer you the granularity you need without waiting for a feature update to the plugin.Just a thought. Would love to hear what you think!

derkork commented 10 months ago

I'm not sure I quite understand the use case behind this. I gathered that you somehow need this for animation control, but I didn't understand how exactly this would help. Could you make some more concrete example of how this would be used?

In general I think it could be helpful to record the time stamp when the state was last entered/left for AI or cooldown purposes (e.g. for making a guard that says "if i have been in state A during the last 5 seconds, i don't want to go into it anymore"), so I wouldn't downright dismiss the idea.

But I really think it helps knowing the use cases to design a solution that really works well in general and not just some niche use case, so I'd appreciate if you could give me some more details on this. Thanks a lot!

Phanterm commented 10 months ago

@BadBoyGodot

Hi Phanterm,I'm not the author of the plugin, but I found your issue really interesting...

I'm not quite sure I understand what you mean by substates! Unless you mean to have a helper state that automatically transitions, but with a delay seconds applied that uses a value which is updated on a per-state basis upon entering them? Would love to know more.

@derkork

I'm not sure I quite understand the use case behind this. I gathered that you...

Sure, and thank you for the response! I'll provide a couple of specific use cases from my old state machine:

Granted, I'm new enough to Godot that I don't have my head wrapped around expressions or guards as of yet, so there are likely possibilities there with Expression Guards. And much of what I described above can be achieved with timers, but for that granular control over how gameplay feels in an action game with myriad different maneuvers and more fluid freedom to "cancel" actions to go to different states, this would require quite a few unique timers. Further, I don't like the idea of relying too much on adding method calls into my animations to control state flow, but that's more because Unity's animator has burned me too many times in the past by being unreliable with firing animation-based events.

In general I think it could be helpful to record the time stamp when the state was last entered/left for AI or cooldown purposes (e.g. for making a guard that says "if i have been in state A during the last 5 seconds, i don't want to go into it anymore"), so I wouldn't downright dismiss the idea.

I would love something like this! Even having it as an additional option like Delay Seconds would be supremely useful to me.

derkork commented 10 months ago

Defining the amount of time an entity is in an invulnerable "hitstun" state upon taking damage, before leaving that state.

Could be done like this: image

Defining a specific window for an attack in a state by comparing time in state ( if (_timeInState >= 0.2 && _timeInState < 0.3). I used this to ensure an attack input performed in a jump state always processed exactly at its apex.

Not sure if the time really helps here, because the "apex" pretty much depends on how you jump and where you jump from. I have a feeling this would be better triggered by checking some velocity to find out if we go up or down.

Creating micro delays for more granular control over gamefeel (i.e., not being able to move immediately after attacking)

This sounds similar to "coyote jump", there is actually an example in the platformer demo for this. It pretty much works the same way as the first picture I showed.

Using these as conditions in simpler states for how long one has to be in a state before the transition would fire

You can already have this by creating a transition with a delay but no event. It will fire delay seconds after entering the state.

Creating windows in which an input in a state caused a different transition (such as pressing an input and then pressing attack within a 0.15s window created upon entering the state)

Also sounds very similar to the "coyote jump" thing.

So i think what you want to do can already be done without extra timers just with what is built in. Which leaves us with the extra guards based on state history. I'll have to think about a nice way on how to add this (as you can temporarily enter/leave states without seeing it and this could become pretty confusing pretty fast).

Phanterm commented 10 months ago

Thanks for the knowledge. I read up a bit more on expressions as well, and I think if I really needed this, I could just set an expression property for time in state by recording it during an on_enter signal for a given state and then validate it for any transitions I'd need. It seems like the rationale is that I should be using more transitions, rather than having multiple areas within a state's code that all route to the same one.

derkork commented 10 months ago

It would look that way, especially since transitions have built-in timers, and can automatically fire after some time, so they can do the heavy lifting without you having to manually track elapsed time.

BadBoyGodot commented 10 months ago

@derkork There may be a more straightforward need to get the remaining time in a delay of a transition, here is a scenario. Character would have 2 states, "skill" and "skill cooldown". To display the cooldown process, dev needs to put remaining time display on the skill icon or display a progress bar with the icon. either way requires a ratio on current cooldown state is how far from leaving.

currently "delay_seconds" can be dynamically set, but "remaining time" of the transition is only available through debugger. I would believe exposing this info should also be useful for players, especially in common game practice. (suppose there is no guard, only a delay set in cooldown state)

good thing is one less timer to track time, but may encourage more talking to the transition nodes instead of statechart.

maxresdefault

would love to get your thoughts!

derkork commented 10 months ago

Hmm how about a transition_pending signal on states, which would fire every frame for as long as a transition is pending for this this state and has the initial and remaining delay as parameters. E.g. like this:

image

This way you have all the information delivered and you don't need to poll the transition, like the debugger does. Plus you still don't directly interact with the nodes of the library.

BadBoyGodot commented 10 months ago

That would be very nice!

I haven't run into performance issues with many signals emited every frame, if that happens maybe a frequency cap is needed or option to turn off the signal. For now I think the pros outweigh the cons and it's all good!

derkork commented 10 months ago

If you give the UI element the proper interface you can even connect the signal directly to the UI element and don't need to run this through some intermediate code like I shown in the example. I think we have a winner here.

BadBoyGodot commented 10 months ago

True indeed.

derkork commented 10 months ago

https://github.com/derkork/godot-statecharts/assets/327257/9df7c3ac-486b-4268-8a9f-7b52a046a539

BadBoyGodot commented 10 months ago

well, this will make skills / abilities implementation simple and clean, and give more info on time in state too.

derkork commented 10 months ago

I've just released 0.7.0 with this new signal + the demo from the video I posted. It will appear in the asset library once it gets approved. Until then you can also download the zip file from GitHub.

BadBoyGodot commented 10 months ago

Nice! It seems version number still set in 0.6.0 though.