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

Suppose I have a neat OO concurrent state machine on a dude with a sword; one multi-knob controlling the movement (walk, run, dash, jump…) and other multi-knob controlling the attacks (kick, slash, block, stomp…). The game designer suddenly introduces two additional effect states - “drunk” and “berserk”. Those two states can also occur simultaneously. Each of those and their combination alters the behavior of all movement and attacks in a different way. How do I solve that?

Btw have you read that Yegge article linked by @renevanderark? You should.

1 Like

Ok, full nerd warning: I love gdscript most because they stripped the superfluous declaration of class and just let you do the thing.

1 Like

If it helps, the more we learn the more we see what we don’t know. That’s why imposter syndrome is a thing, and very prevalent in programmers.

In this definition, I agree, it just seems like regular code. Something I haven’t said yet is that maybe that’s not the problem it was meant to solve. Stripped way down a state machine may look like some if statements

var state_flags: int

func do_something() -> void:
    if state_flags == 0:
        do_thing()
    elif state_flags == 1:
        do_thing_else()

This is pretty typical code. Making it more “state pattern” like gets some code like this.

# 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()

It can still do the same thing but there are benefits gained, mainly that its not hard coded anymore. The machine can be edited at runtime, and its organized due to being divided up.

This is a public discussion, you are free to join and add any input. That is also a perfectly valid answer.

So I will defend it, because I can see its usefulness in cases, but I actually disagree with its use in game characters. I think they are too complex for this pattern. Too many moving parts. I started this discussion to find what people find lacking in it so that I can try and apply those use cases to my own system. I’m also just curious how others structure their characters since it is a complex situation.

I do think game dev can be different from other programing domains, so some patterns will be used more here than other spaces, so maybe you will find your answer here. It does seem to be a pattern game dev is obsessed with, given all the tutorials around it. That could be misplaced hype though. Game dev is also filled with hobbyists and people who are new to programming.

2 Likes

I appreciate this answer, @panthera.

Your first example looks like great maintainable code to me. Why mess with a winning team?

I think that a good plugin or design pattern to manage character states with, if built in a very opinionated way, could be very empowering.

I agree with you that it is interesting to see what approaches there are in the answers to your question do far.

But for simplicity’s sake and usability’s sake, you have to be opinionated: meaning you can’t please everyone.

So if your second example with the validation of something and the inline lambda does indeed work well to solve some issues in a stable and durable way, I’d turn it into a library or a visual plugin to make it reusable; black boxing this code and adding a good safety harness of regression tests.

The first example however… Is self documenting and can also be the result of composite states.

1 Like

At first I skimmed it, now I have read it. Its interesting. I haven’t used Java but I have other languages. It just seems to be showing how language syntax and paradigms change how you do things in those languages. He clearly does not like Java.

I was asking you to show a mock implementation of how you would accomplish something, and I would still like to see that, for the example you just gave.

Given the two state machines (movement and attack), I would say each state needs to check the effects of the character to determine how they act.

I see this results in an multiple if statements, but that’s not exactly a problem. Plus it would clearly show that if the character is drunk then they will act differently, while allowing each state to decide how they act given the data.

1 Like

Interesting lesson I wasn’t expecting, “be opinionated.”

My intention has been to find ways to improve the system I have, but yeah. I can see I need to stat drawing some lines. I am cursed by my curiosity and desire to problem solve however, so even if discussions I have result in no changes to my code, I still want to understand other people’s methods.

I see it as a scalability thing. That code won’t look so good as it grows to cover more, then is decided to change some states that it covers.

Could you show an example of it using composite? I’m not sure how different that would be from my second example.

I’m not gonna write any implementations here. Besides, I already said what’s “my implementation” - I just if it. I may eventually extract abstractions from that if opportunities show up but I keep it flat for as long as possible. What exactly would those abstractions be? - it’s impossible to tell before seeing the actual flat implementation. The problem domain dictates the abstractions. Not the other way around. And game character state problem domain is quite wide and varied.

So if you want to build a universally useful abstraction, you’ll have to make many specific flat implementations and see if common abstraction opportunities arise. Guessing generalizations is a game that’s always lost. Discussing them theoretically is also not far from that.

Who decides which if statements are a “problem” and which are not? The comfort zone of your preconceived abstraction? I’d posit that no if is a problem when branching on state, and none of them need to be abstracted for a successful state management implementation.

What’s the metric used to measure how good the code looks?

1 Like

Writing opinionated code is a design pattern. It’s one of my favorites, but of course works best with people using the software and giving feedback.

Like you said, it means limiting your solution in its capabilities in favor of simplicity of use. And code.

Your first example would be the result of a successful refactor of your second example.

In your first example you extracted away the validation code to a function with an understandable name and purpose, which is invoked before setting a flag that also clearly states what it does.

Whatever the lambda does would ideally also be extracted to a named callable, so we get its purpose. That isn’t in example one yet, but once it’s in there, we all totally get what it’s for.

Even in hypothetical examples, I will only get what you’re trying to tell me when you name things meaningfully.

The names in example one get away with being abstract because the code is so simple and I still get what you’re communicating.

Alright, you have said that. Valid.

I learn best from seeing code, so I wanted to see the way you would do it compared to me. Also as a way to see the “limitation” your code does not have but mine apparently does, or the “flexibility” you claimed it has over mine.

1 Like