Hi everyone, I’d like to share with you my Modular Character Controller. It’s a project I have been working on and is a set of scripts for making characters in your Godot games do the things they should. This applies to player characters, any NPCs, and even vehicles.
I just released a major update for it that includes ways for much better control over the actions a character can make.
Its available in a few places and can be used as a plugin or a project if you want to see the demo characters.
This looks like an implementation of a Statechart. You might also be interested in seeing the Godot StateCharts Plugin. I’d also recommend splitting your examples folder into a 2D and 3D version so it’s easy for someone to copy and paste the appropriate one into their project.
Thanks for the suggestion, I just added a 2d example and in doing so had the same thought of separating them from the actual project, since I intend on adding at least one more example.
After looking through the state chart descriptions I am am left a little confused on how they actually work so I am hesitant to say this but I don’t believe these are the same. They do however share a common goal, which is fighting state explosion, the entire reason I came up with this.
State charts seem to still be built on a state machine but my system technically doesn’t have states. Its more an amalgamation of actions that work together. My goal was to not only separate out responsibility but to get rid of states, especially transition states.
Fair enough. So basically you’re rotating pipelines. It’s an interesting approach. I haven’t decided how I feel about it. I’m still mulling it over. @wchc@normalized I’m curious if you have thoughts on this approach.
I would recommend that you add some icons to your new nodes. I think it would make your screenshots easier to understand, and the nodes themselves easier for others to use.
To be honest I’m still trying to find the best way to describe it in simple terms, I’m also working on some graphics to add to the README file which I hope will help.
Icons are something I want to add as well, just haven’t decided on what they should look like yet.
It is an interesting approach indeed, different from a traditional state machine and state charts (the term I didn’t know about, even though the concept is familiar - thanks @dragonforge-dev for the link).
I tested around a bit and while it does seem to work, I’m not sure about its usability yet. The system seems to be overly complex and hard to fully understand, even though I have to admit it’s very well documented. If I wanted to implement my own new actions - I would probably spend several hours just trying to grasp how the whole system works before I get something working with confidence. I can’t imagine a beginner being able to work with it other than just bluntly copy-pasting it into their project. Maybe that’s just something I’d get used to though, I’d love to play around with this concept one day.
I have a particularly hard time accepting some of your naming decisions @panthera, especially things like ActionCollision, _other_collision._hit_by() etc. which even though it seems proper in the context of this project, it is way too close to other game engine concepts and will just lead to confusion at one point or another.
This collision diagram in the Action Collision part of the documentation made the biggest difference for me in understanding the whole concept, so maybe you could expand on that and provide a couple more examples in the similar form.
All things considered, it is a very impressive piece of engineering @panthera and I will be following the progress!
Talking with other people, it does seem I have made a system that is hard to explain and/or understand, so I am working on that right now. If I can’t explain it to people so they can use it then the system is useless. I do have more diagrams in the works that will be added, I am just doing my best to make the concepts clear.
Collision was a term I stuck with since that part of the system does function as collision, which I actually thought would help in understanding it since it has that similarity to a pre-existing concept.
I seem to be having a hard time explaining that actions do the thing you want your character to do. It might do this by calling other systems on your character, such as the animation tree, or do the logic itself. But to be fair I see actions as orchestrators that get the other systems on a character to work together, like calling the animation tree to play the walking anim and the physics system to make the character move. Kind of like a dynamic interface where actions are the functions.
Also the system does have layers. Minimally you only need to make a controller and actions (input and logic). As your character grows in complexity you may need to use the permission profile or collision for more control over when actions can play. But that should be all. I might try and make this more clear in my next iteration of the documentation so its more beginner friendly.
Anyway I’m glad you find it so interesting and weren’t turned away from the complexity.
Seriously though, I’m glad that the OP realized the ever-touted state machines become inadequate as soon as you begin to require some more sophistication and fluidity. That’s what I’ve been yammering on since forever.
This looks like your regular if-ing on flags, which, in my book, is a perfectly good way to go about it. I didn’t look at it closely, the implementation may be a bit too abstracted for my taste. Managers and Containers easily scare me away.
I realized state machines are not good for characters pretty quickly, which is why this exists.
What do you mean by “this looks like your regular if-ing on flags”?
Also, as of now, I have no idea how I could make this without abstraction, but I am always a fan of simplification. So if you have any ideas on how I could make this less scary that would be appreciated.
The approach looks basically the same as maintaining a bunch of state flags and toggling pieces of code depending on their value - the most intuitive approach there is, just object-orientized a bit.
As for simplifying, start by getting rid of the manager. Or at least don’t call it that. Nobody likes managers.
Now that you mention it I guess it does kind of work that way.
I’m not sure I can get rid of it since it does all the actual work and has some useful functions for actions to use. Do you have an alternate name suggestion? I default to “manager” when an object manages things like this.
Just call it what it is. Usually just a pluralized version of what it tracks or “manages”. For example, an InventoryManager is typically just your Inventory. Your ActionManager is just a collection of Actions. You’ll also notice yourself looking at how the object works differently. A “manager” often dictates how other things should act. It often causes tight coupling. But when you change that to Actions, you may find yourself delegating more responsibility to the individual Action objects it contains.
You guys were helpful and seemed interested so I’m just letting you know I’ve been reworking the README file a lot. Hopefully its more clear about the concepts at work.
I strongly agree with this. It seems trivial but it may simplify your thinking and purge some of the nasty aspects of object orientation dogmas from your design.
I’m not seeing how a different approach would work with Godot’s node system. Nodes are objects and the action nodes are set up so they can be attached to a character but they still need a centralized interface to call them from. By having the manager node that gives me the central interface for using multiple nodes and makes the structure clear in the character scene compared to having the nodes attached to the character without the manager being the parent node.
My thinking may also just be too based in OOP (its all I’ve ever been tough) but I don’t know how to make this system without these two being coupled, at least not without a large rework since Action Nodes are designed to be used by another class.
Also I try to avoid naming things just the plural version since that can easily be misread. I do have an Action Container, which the manager uses, and acts as you’d expect.
It’s a pull state machine where state switching is delegated to the states and the machine itself only handles which state is active.
No they do not. That’s just what you are used to, because you are used to the Manager Anti-Pattern.
There are many ways to handle this kind of connection. An Autoload with signals, assuming the parent node is a centralized source, or recursively looking for a class up or down the tree, to name three.
Actually, your thinking is not very OOP. OOP has four pillars: Encapsulation, Abstraction, Inheritance, and Polymorphism. Having a Manager is an anti-pattern because it causes you to break encapsulation.
Yes, what we are telling you will likely require a rework, but you asked for feedback.
Actually, this is a common programming practice that was popularized by Ruby on Rails. The idea being that if you had to describe a collection of something, pluralizing it made it clear that it was meant to be a collection of singular items. So while it may take some time for you to wrap your head around the idea, it is not likely to be misread by experienced developers.
Except, you could combine the Action Manager and Action Container into one object called Actions - and then delegate things that don’t belong there into the Action objects themselves. Which would improve the object-orientedness of you design by improving encapsulation, and it would sharpen your interfaces (abstraction).
however states handled that way can quickly turn into spaghetti …
In that code, what if the character object has input states and current action states, and the code simply decides whether to change state.
Jump could be an input state, if Input.is_action_just_pressed(“jump”):
and the current character action might be slide.
And what if the variable attack might represent an intention or behaviour state or stance.
Ive worked through loads of spaghetti codes because just trying to get the code working properly was the objective … the code does get easier when states are properly categorized.
Sorry I took so long to respond, I’ve been researching and reevaluating my manager script based on the things you have said.
Changes
I’ve made some changes that move some of the functionality to the action nodes and away from the manager, but it was a small amount. I also realized I am a little unsure what responsibilities action nodes should have exactly, so I was hesitant to make these changes, but I will need to think more on this.
You don’t need to look it over. I’ve moved the action collision logic from the manager to action collision so now nodes are in charge of how they interact with each other and the manager’s only concern is proper selection of action nodes to call on per request.
Manager Anti-Pattern
I’ve also looked at your state machine and found its the same as my action manager. Because of this I was very confused about your comment calling my thinking “not OOP” and how managers were anti-patterns, which was a shock to me since I had never heard this. So I did some research and discovered that technically they are, but it seems like just using the name “manager” can be unclear and lead to improper use. Which is kind of what you said about it causing the breaking of encapsulation.
I agree now that “action manager” could use a name change to be more clear but I just don’t know what would be more clear for both the function it servers and its use in the scene tree. Maybe “Action Coordinator” or something like that.
Slimming Action Manager
Now about combining action manager and action container, I can’t. I’ve removed what I believe I can from action manager, as mentioned in the beginning, and the last big step before it becomes just a container would be removing the filtering using permissions. This just wouldn’t work because its effectively the “state” of the system.
I’ve explored some options for moving this filtering responsibility to action nodes themselves but the system would just not work as it does now and lose a key benefit, which is only one action activates per request. If manager sent out a signal to all actions and they decided if they activated there wouldn’t be a way to prevent multiple actions from activating. Plus it would be unknown which actions get the signal first creating more issues and making things much more complicated for action coordination. If I were to just iterate over each action then stop when one activates, this would be much slower and any form of optimization would just lead back to how it is now. At least from what I can tell.
Summary
I made some changes to action manager (moved some responsibility out of it and to the action nodes) which was good but also found that it works pretty similarly to a state machine.
Did some research into what the “Manager Anti-pattern” was and now I believe a name change is in order but also that the class, as it is now, is still proper oop.
Also I don’t believe I can move more responsibility out of the manager without causing system breaking issues. But I could be wrong.
Anyway, I probably put too much work into this response but learning about the manager anti-pattern was interesting and I appreciate the push for me to more deeply analyze a parts of my system with something to compare it too. I am now putting more thought into what objects should be responsible for what behaviors.
I’m not sure I understand what you mean by input states and action states. Do you have a diagram or example. I’m not sure I see how these go together or work as you are saying.
I also don’t see how this is solving state explosion.
if Input.is_action_pressed("jump"):
jump = true
# we assign the input state to a bool
if Input.is_action_pressed("attack"):
attack = true
# .... later
if jump and not attack:
jumping = true
# now we set a bool for the character
# state
elif jumping:
if attack:
do_jump_attack()
attacking = true
# perhaps we need to check this bool
# above instead of the input
So the variables jump and attack just mimic the input action, but what if they are treated like character states because the effect is the same? The code becomes confusing because there are a lot of booleans with similar names, and when they get reset becomes another problem.
And then theres a bunch of states that overlap these states, especially for AI state machines, where the character might have “mental” states, like “attack” or “defend” that require movement and so the characters state might switch to move with a target … i actually have this problem with one of my game state machines.