0 votes

Apologies if this is an inappropriate category/question for this category.

I'm curious how some of you guys manage your state machine variables shared across different states, particularly for a character? For example, velocity is something you might want to have several states mess with, so how do you allow those states to mess with that variable?

To be clear, I don't mean for the new animation state machine, I'm talking about the state machine design pattern as it applies to godot.

Currently, I just have all those kinds of values stored in the "oldest parent" of the hierarchy so to speak, and all my states reference that parent to read or modify any desirable variable. I was thinking about using some sort of singleton to store it, as that script is getting quite cluttered.. Hoping I can get some insight into how to organize some of this data better, really.

I'm working in GDscript currently, but if it would be smarter to do this sort of thing in C# or C++ I'm comfortable with implementing it in one of those to achieve what I want to achieve in some better way.

Thanks!

asked Apr 26, 2019 in Engine by tyo (42 points)

I think there is an underlying assumption that I want to ask about. Are your states in your state machine individual nodes?

Indeed they are.

Hmm, I do it a totally different way. I build my states out of a combinations of functions and little wrapper objects. Common variables are just part of my player script. State specific variables are usually just part of the functions (which are co-routines). And any variables required for smooth transition are stored in the little wrapper objects.

I wonder if I should post a tutorial.

2 Answers

+1 vote

Currently the way you describe is also the way I do it. I store shared variables in a parent node, or whatever object being separate from the states themselves, and have states modify them.

One way to make this more clean would be to make it so states only have access to the variables relevant to them, so that could be done using wrapper functions or interfaces (but only C# has interfaces). There are many other ways, more or less complicated, ranging from getters to entire dependency frameworks. It depends which problem you are actually facing.

I would not advise singletons or statics unless you want those states to be unique and persist for the entire execution time of the game (including menus, and restart level mechanic, which typically screws you if you abused static variables).

answered Apr 26, 2019 by Zylann (27,008 points)
+1 vote

This might be a good candidate for a blog post, but I will explain how I store variables at the moment. First off, I use a lot of coroutines. For example, this function does most of the work of attacking.

func _punch_think(delta):
    $FlipMe/RunFX.emitting = false
    $AnimationPlayer.play(_think_state.attack_anim)
    while $AnimationPlayer.is_playing() or not is_on_floor():
        vel.y = min(vel.y + grav * delta, -jump_speed)
        if is_on_floor():
            vel.x = sign(vel.x) * max(abs(vel.x) - walk_accel * 0.5 * delta, 0)
        else:
            vel.x = _calc_horizontal_navigation(delta, false)
        vel = move_and_slide(vel, Vector2(0, -1))
        delta = yield()
    swap_think_to(Walk_State.new(self), delta)

So the first thing you notice is the answer to your question, where are the common variables stored. Common variables are just members of the script. State specific variables are just declared locally in the coroutine. For example, vel is a shared variable.

I have some local objects to help things out:

class Think_State:
#warning-ignore:unused_class_variable
    var owner
    var _think_func #function to call if no coroutine state
    var _next_think = null #store coroutine state
#   var _input_func #might be good in the future

    func process(delta):
        if _next_think:
            _next_think = _next_think.resume(delta)
        else:
            _next_think = _think_func.call_func(delta)

    func _cleanup():
        pass

class Punch_State extends Think_State:
#warning-ignore:unused_class_variable
    var str_repr = '[Punch_State %s]' % self
    var attack_anim

    func _init(owner, attack_anim):
        self.owner = owner
        self.attack_anim = attack_anim
        self._think_func = funcref(owner, '_punch_think')

In this example, the constructor is over ridden to store a transition variable in attack_anim.

Then the other critical functions are my process and swap think functions:

func _physics_process(delta):
    if cached_button_input:
        cached_button_input['time'] -= delta
        if cached_button_input['time'] < 0:
            cached_button_input = {}
    if not _think_state:
        swap_think_to(Walk_State.new(self), delta)
    _think_state.process(delta)

func swap_think_to(new_think_state, delta = 0):
    if _think_state:
        _think_state._cleanup()
    _think_state = new_think_state
    if delta > 0:
        _think_state.process(delta)

Don't know if this helps....

answered Apr 27, 2019 by iron_weasel (114 points)

Sounds pretty interesting. I’ll try my hand at implementing something like this in a naked project and see how it compares for my workflow. Appreciate the thorough explination!

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.