Modular Character Controller (v4.4-4.6)

This seems like the appropriate amount of change.

To be clear, the main difference is you call yours a “Manager” and I call mine a “State Machine”. The anti=pattern is using the name Manager itself. Because, as I said, it causes you to think about things in a way where the “Manager” needs to know details about those below it to do its job. In other words, it micro-manages things it should delegate.

The State Pattern was introduced by The Gang of Four in their book Design Patterns. I am implementing the StateMachine object as a State Pattern using the Pull Method. This means that instead of the StateMachine deciding when a state should be entered, the States make that decision. Architecturally, I have separated out each State into its own object, but they operate only inside of a State Machine object, and are therefore all conceptually part of the same object and pattern. Hence the naming convention of StateMachine and State. Technically, you could also look at the State objects as implementations of the Decorator Pattern.

You saw the effects of the Manager Anti-Pattern in your code. Because once you looked at it, you moved the collision logic into the Action nodes, and out of the Manager. This is a perfect example of how knowing that the word “Manager” is an anti-pattern can prevent you from falling into the trap. There was a great C+++ conference talk on this years ago. Sadly I can no longer find it on YouTube. In it, the speaker talked about the use of the word “Manager” and how to eliminate it.

If you take a look at the Active Record Documentation (which is the data implementation convention for Ruby on Rails), you will see that data objects are singular PascalCase, and the tables containing them are plural snake_case. So a Book is contained in a table named books. This same logic can be used in general programming, and allows you to think about things in a new way.

PlayerStatistics Detour

Let’s take the example of player statistics. Things like number of times a player jumps, coins collected, etc. I want to track any number of statistics across the game. My brain says, “I need a way to manage this.” So I take a step back and say what does one statistic look like?

A statistic must:

  1. Have a unique name.
  2. Be stored by game level. (For example, I want to be able to know the number of times the player jumped in the jungle level vs the floating island level.)
  3. Store its value between game sessions. (Needs to be stored on disk.)
  4. Only go up. (Stats never go down, if there are values that can go down, they are not statistics.)

So, I could create a StatisticManager. But following the Active Record Method, if one is a PlayerStatistic, then a collection would be player_statistics - however, since I’m not storing them in a database table, but an object, the collection becomes PlayerStatistics.

So let’s look at an implementation for that class I made for a game I’m working on.

class_name PlayerStatistics extends Resource

var stats: Dictionary


## Adds the passed value to the statistic named, based on the current level.
## If the statistic or level doesn't exist, a new entry is created.
func add(statistic_name: String, new_value: int) -> void:
	var category: Dictionary = stats.get_or_add(statistic_name, {Game.level_name: 0})
	var value: int = category.get_or_add(Game.level_name, 0)
	value += new_value
	category[Game.level_name] = value
	print(stats)


## Gets a list of all the categories in which statistics are stored.
func get_list() -> Array:
	var list: Array[String]
	for key: String in stats:
		list.append(key)
	return list


## Retrieves the aggregate value for a statistic across all levels played.
func retrieve(statistic_name: String) -> int:
	var category: Dictionary = stats.get(statistic_name)
	var total: int = 0
	for key: String in category:
		total += category[key]
	return total

Basically, it’s just a Dictionary of Dictionaries. You’ll note I don’t do any saving to disk here. Instead, that happens in the UserProfile object. The UserProfile handles all of the saving and loading of statistics because it has a PlayerStatistics object that it saves and loads. And the Player object has a PlayerStatistics object that is just a reference to the UserProfile object’s PlayerStatistics object. This distinction wouldn’t be necessary in most games, but since this is a multiplayer game, I will have multiple Player objects in a game and only want to track the stats of the User in the game.

Back to Renaming Action Manager

Back to your class, currently named Action Manager. You posited you could rename it Action Coordinator. This is a semantic name change that does not avoid the Manager Anti-Pattern - because you are still thinking of it as a manager, or coordinator, or director, etc.

I took a look at your ActionManager and ActionContainer code. I can see why you think they need to be separate, because ActionManager contains an ActionContainer instance. And it is a logical OOP breakdown. It is not strictly necessary, because ActionContainer is the data, and ActionManager is the logic, and neither functions without the other. But if you like it, ok.


Access Modifiers

A note on your ActionManager code and access modifiers.

You have indicated in your code that private functions are in a section where every function name starts with two underscores instead of one. Then it looks like you are using one underscore to indicate protected functions. I mention this because it is not the Godot convention as set out by the GDScript Style Guide. Since you are making a plugin, it is important to note that making up your own conventions is going to A) obfuscate your code, and B) create hurdles for people contributing to your code.

GDScript does not support access modifiers. Everything in a script is public. Period. The convention is to prepend private functions with an underscore to indicate that they are only to be used inside the class. Likewise, class variable names are prepended to indicate the same thing. Function arguments are prepended with an underscore to indicate they are not used in the function. (This is typically for functions called by signals that pass arguments you do not need.)

Before, you had said that you do not see a difference between my code and yours. You’ll note if you look at State, that it calls a private variable inside itself to reference the StateMachine, but calls a public function.

## Asks the [StateMachine] to switch to this [State]. Should always be used
## instead of calling _enter_state() when a [State] wants to switch to itself.
func switch_state() -> void:
	_state_machine.switch_state(self)


## Asks the [StateMachine] to exit this [State] if it is the current [State].
## Should always be used instead of calling _exit_state() when a [State] wants
## to exit. Will fail if the current [State] is not this one.
func clear_state() -> void:
	if is_current_state():
		_state_machine.clear_state()


## Returns true if this is the current [State].
func is_current_state() -> bool:
	return _state_machine.is_current_state(self)

In some cases, those StateMachine functions are purely helper functions intended to prevent the breaking of encapsulation by providing an abstracted interface.

## Should ideally be called from [method State.is_current_state][br][br]
## Returns true if the passed [State] is the current [State].
func is_current_state(state: State) -> bool:
	return _current_state == state


## Should ideally be called from [method State.clear_state][br][br]
## Exits, then clears the current state and sets [member State.can_transition]
## to true for that state.
func clear_state() -> void:
	if _current_state:
		_current_state.can_transition = true #Reset this so we don't get frozen out of the state if it's set to false
		_current_state._exit_state() # Run the exit code for the current state. (Even if the state says you can't exit it.)
		_current_state = null

Your code introduces a protected access modifier that GDScript has no concept of, either in code or convention. It also replaces the private access modifier convention. I would recommend that you use the private access modifier convention and make your protected functions public. You can still indicate how they should be used in documentation.

@export Variable Note

Also, I notice that you have @export variables that are private. Again, this breaks convention - since an @export variable can be changed in the Inspector, it is by definition public. If you want it to be private, I recommend getting rid of the @export notation, or creating a private version of the variable that gets its value from the @export variable interface.


Back to Renaming Action Manager

So looking at your code for Action Manager, it has two public functions play_action() and stop_action(); and five protected functions _play_private_action(), _add_action(), _remove_action(), _get_action(), _get_context().

I list these because a good way of picking a class name is to make the class name and function names self-documenting code. So let’s try renaming Action Manager to Actions as I recommended before.

  • ActionManager.play_action() becomes Actions.play(). Because we know that Actions if we play it, is going to play an action.
  • ActionManager.stop_action() becomes Actions.stop(). Because we know that Actions if we stop it, is going to stop an action.
  • ActionManager._play_private_action() becomes Actions._play(). Because we know that an underscore indicates a private function.
  • ActionManager._add_action() becomes Actions.add(). Because we know that adding to Actions adds an action. And it doesn’t need to be private because theoretically anyone can add an action and it should not derail the object.
  • ActionManager._remove_action() becomes Actions.remove(). Because we know that the only thing we can remove from Actions is an action. And it doesn’t need to be private because anyone removing an action will not cause it to break.
  • ActionManager._get_context() becomes Actions.get_permissions(). Because it seems this function is primarily used to see what permissions are available and so it’s a more descriptive name. It does not need to be private, because we are just returning information. It’s a getter. Anyone can do what they want with that information.

Perhaps Actions seems like a slightly odd name, so what does Action Manager really do? Well, it plays and stops Actions So ActionPlayer would follow the naming convention of things like AudioStreamPlayer and AnimationPlayer.

  • ActionPlayer.play()
  • ActionPlayer.stop()
  • ActionPlayer._play()
  • ActionPlayer.add()
  • ActionPlayer.remove()
  • ActionPlayer.get_permissions()

In this case, maybe it makes sense to rename ActionContainer to Actions because it’s just a group of actions with some accessors.

  • ActionContainer._to_string() becomes Actions.to_string(). Because there’s no reason to make conversion methods prtivate.
  • ActionContainer.add_action() becomes Actions.add().
  • ActionContainer.remove_action() becomes Actions.remove().
  • ActionContainer.get_actions_by_type() becomes Actions.get_by_type().
  • ActionContainer.get_action_by_name() becomes Actions.get_by_name().
  • ActionContainer.has_name() becomes Actions.has_name().
  • ActionContainer.has_type() becomes Actions.has_type().
  • ActionContainer._sort_action() becomes Actions._sort().

But if you want Actions to replace ActionManager or you want to follow Godot conventions and are using ActionPlayer, you could also call this ActionLibrary.

  • ActionContainer._to_string() becomes ActionLibrary.to_string().
  • ActionContainer.add_action() becomes ActionLibrary.add().
  • ActionContainer.remove_action() becomes ActionLibrary.remove().
  • ActionContainer.get_actions_by_type() becomes ActionLibrary.get_by_type().
  • ActionContainer.get_action_by_name() becomes ActionLibrary.get_by_name().
  • ActionContainer.has_name() becomes ActionLibrary.has_name().
  • ActionContainer.has_type() becomes ActionLibrary.has_type().
  • ActionContainer._sort_action() becomes ActionLibrary._sort().

Alternately you could leave it ActionContainer but consider removing the redundant part of function names. (Namely the overuse of the word “action”.)


The fact that you’re using the term “state” seems to indicates that underneath the naming convention, you’re following the State Pattern. Which your code seems to back up.

Your ActionContainer code is literally a single Array and a bunch of access methods for that Array. It could easily be incorporated into the ActionManager if you wanted. It doesn’t have to be though. If you combined them and just called them Actions, it would look like this:

  • Actions.play()
  • Actions.stop()
  • Actions._play()
  • Actions.add() (Combine the two, or make the one from ActionContainer private and call it.)
  • Actions.remove() (Combine the two, or make the one from ActionContainer private and call it.)
  • Actions.get_permissions()
  • Actions.to_string()
  • Actions.get_by_type().
  • Actions.get_by_name().
  • Actions.has_name().
  • Actions.has_type().
  • Actions._sort().

I don’t think you need to do this. It is in fact a state machine that determines what state(s) are active. Nothing wrong with that.

You get out of your post what you put into them.

2 Likes

Ok, so I’m still confused about your original point of “state explosion can be handled with logic”. It seems like you are showing how logic can be used but that it creates a mess in the code that is hard to understand and maintain. If I am wrong let me know, I could be misreading something and might need an example of logic working to prevent state explosion.

Ok, so state explosion is the effect when two states want to activate at the same time, like ‘jump’ and ‘slide’ and the handling becomes an issue - is my terminology correct? Ive always been able to handle the increasing complexity with logic like:

if jump and not slide:

but the cases get rather complex, hence the term explosion …?

You might see something like

if jump and not slide and anim_time>1.5 and can_reach and right_foot_down and on_floor and etc:

And that logic is harder … thats the point. So its good to classify state types and clarify the code. Once modules are working they can be wrapped up with simpler interfaces .

State Pattern

This is interesting to me because, from a quick google search, I can’t really tell if there is a difference between State Pattern and State Machine. I suspect its a case of state machine is an implementation of state pattern, but people online seem to use these interchangeably.

I am also curious if you would consider my system as using the state pattern. Action Nodes seem like they could be states but also not really since they work together to create a state. So the state is not really created in the same way and also has no class defining it, rather it is just the concept of actions forming it. But I can also see actions as states since they function nearly the same, yet multiple may be active and work together which seems like it would not follow the pattern.

I know you said I am following the state pattern due to my use of the term “state”, but is that enough to qualify the pattern being used? What do you think? It feels more composite or decorator to me.

Action Manager Renaming

I like your breakdown of the functions an name suggestions. Very thought out and more than expected, so thank you.

ActionPlayer seems like a great fit since it follows some of Godot’s already existing names and shows what the class is for. I will also be keeping ActionContainer the way it is since it helps reduce the line count of ActionManager and I see no real reason to merge the two.

Regarding the _to_string() and _play_private_action() funcs, I added the underscore to _to_string() since Godot did not let me override the existing to_string() func they already have,but I suppose I should just change the name to something else to differentiate the two. (I actually can’t remember if it wasn’t allowed or I just wanted to keep the original func still available.)

For _play_private_action() this is a function to play a specific type of action. I am unsure if you are suggesting to just combine the two play funcs or missed that concept since I don’t think its well documented. I have “private actions” which are actions that can only be used within the system and are not ever accessible through requests, the same way private functions should not be accessible outside a class. Due to this I would not want to combine the two functions which could expose these to a Controller or any other outside object.

Access Modifiers

While I know everything in GDScript is “public”, same as python, they do make use of the single underscore to indicate “private” functions to the developer as more just a naming convention. I didn’t think I was breaking convention too much as I thought of it as “levels of privateness”, which I see now can be confusing when next to Godot’s use of the underscore. I’m also use to languages with actual private functions and variables so it didn’t seem like much of a stretch to add.

I used this as it seemed like the most straight forward way to indicate functions that are meant to only be used within the action system and functions meant for only in the class. The auto fill in the editor also supported this since it hid funcs and vars with an underscore till I input an underscore, then still hid the funcs with double underscore till two underscores were input. Seemed like a good idea.

In the case of ActionManager it was a way to control the functions a Controller would have access to since they should only be playing and stopping actions. For ActionNode it was to make the override functions clear while still keeping the functions that should not be used outside of ActionNode clear.

I don’t like the thought of some of this becoming public due to easy misuse, and my mind goes toward the use of an interface class, specifically between Controller and ActionManager, but this may be messy and add an unneeded layer to the system.

I have new things to think about.

2 Likes

I’ve only recently learned the term but my understanding comes from this definition which says state explosion is when a state machine explodes in the number of states it has, not a logic issue breaking the machine. So more the need for transition states and states that are combinations of other states for when you want to do two things at the same time.

I’ve definitely seen this kind of logic in a past project of mine that used a typical state machine. It’s one of the reasons I am making this system.

2 Likes

So the term Finite State Machine (FSM) became popular in the 80s and 90s as the way to program a game loop for a game. Nowadays it is often shortened to State Machine. Technically a State Machine is an implementation of the State Pattern.

You could be using the Composite Pattern or Decorator Pattern pattern to implement the State Pattern. Technically I am, as I have a Node-Based State Machine.

I definitely think you are using a State Pattern, whether intentionally or not. I just wouldn’t call it a typical implementation of a State Machine. Your architecture of Actions distributes it somewhat, but you achieve the same thing. Though as I said in my first post, some of what you are doing seems similar to the State Chart concept in execution.

I handle the same thing with multiple StateMachine objects. For example, I will have a Player Movement State Machine and a Player Action State Machine. This way a player can walk, run or jump, and also attack, block, or use a potion at the same time.

I just looked it up, and apparently you did it correctly, as you were overriding the to_string() implementation. Per the docs:

So you did right. And it makes sense that it’s at the top of the file, as that’s where overrides are supposed to go. Apologies for getting it wrong. I believe that changed in the last version or so, as I do not believe it worked that way in 4.4.1.

Fair enough. But again _play() communicates all of that.

You do you. If you really want to self-indicate protected, I recommend using double underscore for that. Even though it seems less intuitive, it will be clearer to anyone else reading your code because of the private convention of a single underscore. But again, it depends on whether you ever want anyone else in your code on whether it matters if you follow these conventions.

If it helps you, cool. Just keep in mind you cannot control what people do with your code. In my opinion you have overengineered this a bit. But that’s ok, because as you use it you’ll see where things work and don’t.

I’ve done some massive overhauls of StateMachine even though I wanted to keep it as lightweight as possible. @wchc started doing PRs and we started talking, and after accepting one PR and discussing another, we came came up with two more ways to do the same thing. Then we settled on the fourth implementation and that’s what’s in there now.

2 Likes

Yeah thats clear…. the combinatorial increase in states makes the code harder to read and debug and thats when there is a risk that problems occur.

Hence the need to reduce complexity and hide functionality in stable modular objects.