(Updated: How do you design your character architecture?) State Machines for Characters and their drawbacks - What are your thoughts?

So something I do differently. I agree with the animation naming, its clear and makes sense, but I view logical states seperat from animations. Sort of like the action happens and animation follows.

So with the broken down states you gave as an example, the movement state, action state, and hold item. I’d say do that but then select the animation based on those variables. Logic is still separated and there are less logical states (throwing rat while airborne, throwing rat while grounded, …) but the animations are still specific. This would also mean just adjusting necessary variables within the logical state like throw velocity if items have specific velocities. So state is the same and covers multiple animations, only changing variables.

Thoughts?

1 Like

Thoughts?

I think I simply agree with this. :slight_smile:

I always need a lot of words and leave out the punch line, but I think my implementations allow for what you suggest.

The only thing I really put on top of that is one single atomic state for each animation with a long descriptive name.

This one state makes it super duper easy to test the animation in isolation:

And the long name makes code more easy to read and to find.

Sure, but then an if statement can as easily map a single flag. So why make a whole system of abstractions to do that, and still skip the support for mapping multiple flags?

Are you not aware of the fact that the thing your system is doing is basically just mapping flag states to code branches? Ditto for the classic FSM.

I’m confused about what you mean by mapping flags to states and how a state can only represent one (both in a FSM and my system).

const enum: uint64_t {
	ENABLE_VIRTUAL_LEN =		1LL << 0, # flag 1
	FACING_FORWARD =			1LL << 1, # flag 2
	REVERSE =					1LL << 2, # flag 3
	AIRBORNE =					1LL << 3,
	JUMPING_UP_SLOPE =			1LL << 4,
	LANDED =					1LL << 5,
    ...
};

Each line is a single flag. A state would not be mapped to a single one but rather the entire set.

state_1 = 010110, state_2 = 01001

A state could also be mapped to multiple sets that define if the state can be entered/set as active.

func can_enter_state():
    return current_bitflags == 010110 or current_bitflags == 01001

This is also what you mean by mapping code branches to flags right? A state is just a code branch and the flags are used to chose the state?

We probably need to get on the same page about flag mapping to code branches but I still don’t see how my system, or a fsm, is limited to mapping a single flag per branch given what I just said. How have I skipped this?

1 Like

In a concurrent fsm, how do you tell it to execute some specific piece of code only if one sub-state is A and another sub-state is not B? How do you do it in your system? That’s what I meant by mapping multiple flags to a single code branch.

By “flags” I don’t mean strictly binary flags. A flag can be a state of a multi-state knob. But even that can be broken down to mutually exclusive binary flags. So the essence can be reduced to branching on binary flags. Flags are what de facto determines the state of the state machine.

I find it strange that you’re seemingly not aware that the main purpose of state machines is branching the code depending on knob settings. The knobs may be implicit or abstracted away depending on implementation, but that’s the purpose of state machines, when you strip off all the abstraction.

That’s why it’s important to not start abstracted, so you’re aware what is actually happening in the code and what exactly you’re abstracting.

2 Likes

I have always struggled to understand in what what way State Machine is a useful metaphor.

I get a finite state and preventing circular dependency by having things be in some reliable ‘end state’, but I really never understood what “Machine” even means in all this.

1 Like

Soda or candy machine is basically what a real FSM is : it needs clearly defined states and transitions so there is no bugs.

Funnily, analog and electronic versions share the same problem : goodies getting stuck in the machine and keeping your money :money_mouth_face:

Cheers !

1 Like

Well the “machine” part is typically the class that holds the states, so it can be thought of as the “thing”. And looking at the definition of a “machine” in engineering terms,

A machine is a thermodynamic system that uses power to apply forces and control movement to perform an action. (Wikipedia)

the machine in a software setting would probably be “thing that does an action”. Very generic and not clear, I know. The machine is just a class that performs an action when asked. The states just change how the task is done.

This is probably made more unclear in a game context though since we are making objects with machines in them so there is a lot of layers. Real world comparison could be a sewing machine. Its action is to sew but it has states inactive and active, depending on if its powered and the pedal/switch is toggled. It will sew when asked or it wont.

1 Like

It’s useful for teaching kids about the general concept of state… and for coding the logic of actual wending machines :smiley:

1 Like

Sewing machine is much more complex than that. I even doubt it can be adequately modelled using the state machine pattern alone.

I’m aware of the purpose of a state machine, its you I’ve been trying to understand so that I can get to the bottom of if my systems really has this limitation you claim. You’ve just been unclear about this limitation.

You’ve only been saying things but not showing, so its been unclear to me. That’s also why I was trying to see what you mean by “flag” since a flag is already a thing but can mean multiple things. I’m trying to understand the problem here.

Ok, so you do mean “flags” as a representation of data. What still confuses me is your claim that state machines and my system “map a single state flag to a single code branch”. From what I can tell you are saying only a single point of data can lead to a code branch. This is why I tried to point out just a single if disproves that because you can put anything in there and get data from other places too.

# simple state machine broken down without abstraction
# behavior is changed based on internal state, or any other data/"flags"
var state_flags: int

func do_something() -> void:
    if state_flags == 0:
        do_thing_0()
    elif state_flags == 1 or character.velocity > 5: # multiple "flags"
        do_thing_1()

Funnily enough though flags are also an abstraction since they represent other data, that’s actually something I tried to point out when you originally shared your flags and I said:

I find it strange that we are seemingly talking about basic programming concepts ifs and bools, but that’s what you are saying behavior management boils down to. I get that. I still don’t get the limitation you see though.

Now you are including sub-states into the conversation. I’m going to need you tell me what you mean by “sub-state”. I can’t answer a question you keep changing.

Or at least give a really clear scenario that demonstrates what you are trying to say my system fails to do so I can build it. Or maybe show the part of your script that you think my system is “abstracting away.”

1 Like

Examples aren’t meant to be exhaustive, just help in understanding a concept.

1 Like

Yeah, but where’s the encapsulated OO state machine there, with branches abstracted into virtually called functors? That’s my whole point - you don’t really need any of those fancy abstractions. The abstractions cause limitations. Without them you retain maximal flexibility.

Note that the question was about a concurrent state machine. A sub-state is a single state machine within a concurrent state machine, or a single multi-value switch within a system of arbitrary number of switches. Your stove is a concurrent state machine. Each knob is a sub-state that itself can be modelled as a plain fsm. Together they constitute a concurrent state machine.

A stove can’t do anything that depends on the state of two or three knobs. In games we often need to model stoves that can do specific things depending on the exact state of multiple knobs. In those cases, I’d need to do an explicit if exception at multiple places in the code if I was using your system or a standard fsm. Or I could just do it without any system at all. It’d cost me less code. So why bloat my code by using any such system, if I can accomplish the same thing by not using a system?

Btw, don’t get caught up with implementation specifics of the example I posted. It’s just for the overall illustration of what can constitute a state that code can branch on. The implementation is in C++ so node-based GDScript “architecture” doesn’t apply, and is really irrelevant here.

1 Like

Soda or candy machine is basically what a real FSM is : it needs clearly defined states and transitions so there is no bugs.

Who knew! I must’ve missed that lesson. :person_shrugging::smiling_face:

I find I’m fascinated by this forum discussion. In another thread I mentioned the 100.000 hours under my belt. Plowing through quick basic, c/c++, prolog, Haskell, xslt, ruby, php, Java 5 up to 21, python 2 and 3, js, ts and now gdscript. 30.000 Euro in student loan debt to boot.

And all this time I remained dumbfounded by what the hell my teacher in bayesian statistics and machine learning meant by: finite state machine.

The more I learn about programming, the less convinced I become about understanding any of it.

Up to the point that I started thinking it must be me (when they started asking my wise opinions about “AI”). I didn’t have any, they lost me, I must’ve been prompting it wrong.

My best understanding of all this FSM was: must be like the elegance of php’s die("do it early");

Anyway. Thank you. It’s a machine, make sure the candy doesn’t get stuck.

So all names for things in code are a metaphor. Some work well, some are obfuscating. I noticed my managers always use tech jargon against me to prove their intelligence to guard against my petulant simplicity. Especially the vague stuff: “How dare you break complex problems down, we need you mystified. That way we do not have to give you a bonus, a promotion or a raise. Stay in your corner you programming simpleton.”

It’s not always me that must be dumb. So sometimes it’s just the poor metaphors… Unless they’re tightly explained, then we can think of a better one; or drop it.

I like the elegance of saving the candy :candy::lollipop: from getting stuck.

Thank you. :folded_hands:

3 Likes

I would have quoted Yegge’s poem “execution in the Kingdom of nouns”. Used to be my favorite. But we lost him to an LLM one shot, so he’s a crypto grifter now.

I say this with love in my heart.

Edit: I’m sorry. I need to clarify.

There is a lot about programming I still do not grasp after years of professional experience. Sometimes it is really hard to grasp whether something is really “a thing all unto its own” or just another name for a thing I’ve been doing.

This concept (fsm) always holds a special sweet spot for me between something useful and something vague…it’s one of those things that draw out misunderstandings and long explanations.

I do notice the growth in embracing my uncertainties without feeling insecure about them.

3 Likes

Hilarious: “have you tried the :+1: button?”

1 Like

I lol’d several times reading it. Too bad “ai” took the best of him.

2 Likes

Ok, now we are back to abstraction. I’ve already said

But that’s also not what I was responding to when I sent the code. I was trying to find this limitation you keep saying exists.

# just one way to make a state machine with abstraction
var state_flags: int
var states: Array[State]
var current: int 

func do_something() -> void:
    if states[current].valid(state_flags):
        states[current].do_something()
    else:
        var new_current = states.find_custom(func(state): return state.valid(state_flags))
        if new_current != -1:
            current = new_current
            states[current].do_something()

Sure you don’t “need” abstraction, but it sure has its benefits, looking at this code and the last I sent. One is bound to what is in the script and the other can be heavily modified at any time while allowing for data to be organized to where it is needed and used.

You say that but still not have shown this. The opposite seems to be true so far, monolithic scripts and all that.

The answer to this is again the benefits of abstraction, which is weirdly what you claim is lost, and also what I said I am not here to discuss. I would like to address the “limitation”, but if this is actually just me trying to convince you the ways in which abstraction and other OOP principles are good, then I am done.

# stove
var burners: Array[BurnerFSM]

# somewhere 
if burners[1].state == Low and burners[0].state == Hi:
    do_thing()

or

# specific burner
var state = {Low, Med, Hi}

# somewhere
if parent.get_left_burner().state == Low:
    do_something()

I don’t see why an if would be in multiple places.

Can you show me what this would look like to you? And how you save on an if statement that would be in multiple places.

1 Like

This is a natural feeling I think, at least one I have felt too. I think the problem is that patterns are not all implemented the same way, so it can seem like a vague concept, because it is. There are implementations you can find and see as examples but they get modified in practice. I try to see the idea behind them.

This may add to the vague descriptions but a state machine is just a class that changes internal behavior based on its “state.” Its state can be a single bool, a bunch of variables, or even just a string. Its function names and arguments stay the same but the output changes based on the state.

If you’ve seen my last few responses to Normalized, you can see I have two versions there.

I’m open to discuss, but I lack “professional” experience, but I do have a degree in CS at least and years of game projects.

1 Like

Being paid for something does not make someone good. I’m a professional because I have a job in programming.

I am not a professional game developer, just someone who built small things that work well but don’t sell.

That being said, the way I meant it was: all the hours of practice in the world don’t give confidence.

My responses do also illustrate my dislike for the FSM concept, because I do not understand the problem it solves. I and I keep trying to find this problem but I just don’t see it.

That feels off to me. It’s not a design pattern (for me).

A class instance having to act out certain things based on certain compositions of state just sounds like code to me.

I hope you’re still okay with me trying to partake in answering your original question: “What are some difficulties you have faced with the pattern or aspects you don’t like?”

Even if the answer is: I do not like it because the problem it solves remains unclear to me after decades of reading people’s discussions about it.

Because this is a game dev forum and I’m not a very experienced game dev I was hoping for a more satisfying answer to that question. This is because especially game developers seem to keep saying: " I really need to learn how to build state machines."

So that’s me thinking it’s a game dev thing.

I have Computational Linguistics degree, btw not formal Computer Science. Hence the feelings of inferiority.

2 Likes