Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request: Time in State, Time Elapsed #46

Closed
Phanterm opened this issue Oct 5, 2023 · 15 comments
Closed

Request: Time in State, Time Elapsed #46

Phanterm opened this issue Oct 5, 2023 · 15 comments
Labels
enhancement New feature or request

Comments

@Phanterm
Copy link

Phanterm commented Oct 5, 2023

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
Copy link

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
Copy link
Owner

derkork commented Oct 5, 2023

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!

@derkork derkork added enhancement New feature or request question Further information is requested labels Oct 5, 2023
@Phanterm
Copy link
Author

Phanterm commented Oct 6, 2023

@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:

  • Defining the amount of time an entity is in an invulnerable "hitstun" state upon taking damage, before leaving that state.
  • 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.
  • Creating micro delays for more granular control over gamefeel (i.e., not being able to move immediately after attacking)
  • Using these as conditions in simpler states for how long one has to be in a state before the transition would fire
  • 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)

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
Copy link
Owner

derkork commented Oct 6, 2023

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
Copy link
Author

Phanterm commented Oct 6, 2023

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
Copy link
Owner

derkork commented Oct 6, 2023

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
Copy link

BadBoyGodot commented Oct 7, 2023

@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
Copy link
Owner

derkork commented Oct 7, 2023

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
Copy link

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
Copy link
Owner

derkork commented Oct 7, 2023

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
Copy link

True indeed.

@derkork
Copy link
Owner

derkork commented Oct 7, 2023

2023-10-07_11-57-43.mp4

@BadBoyGodot
Copy link

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

@derkork derkork closed this as completed in 3be84f3 Oct 7, 2023
@derkork derkork removed the question Further information is requested label Oct 7, 2023
@derkork
Copy link
Owner

derkork commented Oct 7, 2023

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants