This package provides an (opinionated) framework for developing component-based entity controllers (player controllers being the primary focus).
The package depends on GodotSharp 4.0.0 so it should be compatible with all Godot 4.x versions.
Warning
Upon installing the NuGet package, library files will be automatically placed in your project folder. Check the Installation section below for more info.
This library combines the concept of components (something that all normal game engines have and Godot hasn't) and the State pattern. You add your component nodes as children to your entity node and they perform your entity's logic.
Two types of components are provided: States and Abilities. The main difference between them is that only one state can be (and will always be) active at any moment, whereas abilities can be individually enabled and disabled. State switching and component enabling/disabling is done by calling methods from withing the actual components (more on this in the Code section).
When the scene (with the entity in it) is loaded, all entity's states, except the first one, will be disabled. All abilities that do not have their IsEnabledByDefault property checked in the editor will also be disabled.
When a component is disabled it is removed from the scene tree but kept in memory.
Let's look at an example scene to see how a player controller for a simple side-scroller can be set up:
-
Entity:
CharacterBody2D -
States
-
IdlingDoes not contain any logic.
-
MovingUses the provided movement input to calculate the player's speed and sets the
Velocityproperty. If the movement input is0, switches toIdling. If no floor is detected under the player, switches toFalling. -
JumpingOn enter, adds a vertical impulse to the player. Gradually decelerates the player for a desired jump arc. When the velocity becomes negative, switches the state to
Falling. -
FallingAccelerates the player downward. When a floor is detected under the player, switches the state to
Idling.
-
-
Abilities
-
ReceiveInputUses the
Inputclass or overrides_*Inputmethods to read user input. Forwards the input to other Abilities by setting properties (or calling methods) on them. -
MoveWhen the necessary conditions are met (the player is
Idlingand the received movement input is not0) switches the state toMoving. -
JumpWhen the necessary conditions are met (the player is either
IdlingorMovingand the jump input is pressed) switches the state toJumping. -
ControlCameraUpdates the camera position to follow the player.
-
As you can see, the Abilities are generally used to handle input and switch the state in response to player actions and other events. The States simply modify the player's properties in a most straightforward way, transitioning to the next logical state when necessary. This approach makes it quite easy to extend the player controller with new features. For example, if the developer wants to implement variable jump height, all they need to do is add a bit of logic to the Jump ability to switch from the Jumping state to the Falling state early - when the jump input is released.
Check out this parkour game I'm working on if you'd like to see a more complex player controller example.
The class hierarchy in a project using this library will look something like this (highlighted classes are scripts you create yourself):
Node
├── ComponentBase
│ ├── StateBase
│ │ └── MyCoolStateBase
│ │ ├── State1
│ │ └── State2
│ └── AbilityBase
│ └── MyCoolAbilityBase
│ ├── Ability1
│ └── Ability2
└── ...
└── MyCoolEntity : IEntity
The States and Abilities are provided in a form of abstract StateBase and AbilityBase classes, which you need to extend. Both of them inherit from ComponentBase which contains most of this library's logic. ComponentBase inherits from plain Node. The entity that components are attached to can be anything you want as long it's type is derived from Node and has the empty IEntity marker interface implemented.
So the general workflow goes like this:
- Create a
MyCoolEntityscript derived fromNodeand add theIEntityinterface. - Create abstract
MyCoolEntityStateBaseand/orMyCoolEntityAbilityBaseclasses by extendingStateBase<>orAbilityBase<>; the generic parameter will be theMyCoolEntitytype. - Create the actual states and abilities by inheriting these classes.
- Set up a node tree similar to the one on the screenshot above and attach your entity and custom component scripts to the nodes (ensure that your initial player state is placed first in the states container).
- On each ability that needs to be active from the beginning (in the above example that's all of them) set the
IsEnabledByDefaultproperty totrue.
In order to write your component's logic you should override the custom and existing Godot lifecycle methods.
For abilities the custom lifecycle events are:
Enabled(). Called after the ability has been re-added to the tree.Disabled(). Called before the ability is removed from the tree.
For states the custom lifecycle events are:
Enter()Exit()
The state switch flow goes like this (StateSwitchDatas will be explained later):
CurrentStateSwitchDatavalue is updated.Exitis called on the current state.- Current state is removed from the tree.
- New state is added to the tree.
Enteris called on the new state.CurrentStateSwitchDatavalue is cleared.LastStateSwitchDatavalue is updated.
Important
Avoid overriding anything that begins with an underscore _. This is also true for some built-in lifecycle methods, such as _Ready and _Process. They have custom versions that you can override - OnReady, OnProcess etc. If there is no custom On* version then it's okay to override the base method.
Classes inheriting from StateBase or AbilityBase also have a number of utility methods and properties available to them. You will be mainly working with:
Entity- get the Entity this component is attached to.CurrentState- get the current state.EnabledAbilitiesandDisabledAbilities- get a filtered list of abilities.GetState<TState>(),GetAbility<TAbility>()- get state/ability of the specified type.Can<TAbility>()- returnstrueif the specified ability is enabled.QueueStateSwitch<TState>(),SwitchState<TState>()- change the entity's state. Queueing a new state means the switch will happen after the frame process is done (deferred call) and is generally the preferred approach.ToggleAbility<TAbility>,EnableAbility<TAbility>(),DisableAbility<TAbility>()- self-explanatory.- Check the source code to see what other methods are available to you.
Note
You should prefer the generic, non-null-returning versions of the methods mentioned above. This is because ideally you will know in advance which components exist on your entity. I don't exclude the possibility that you might want to manually register/de-register components, in which case you may want to use the other versions of those methods.
Classes inheriting from AbilityBase also have access to:
IsEnabledByDefaultproperty. Set this totruein the editor on all abilities that should be available to the player when the player scene is loaded.BlacklistedStatesandWhitelistedStatesproperties. Set these in editor and use theIsInBlacklistedStateandIsInWhitelistedStategetters when coding your ability (these properties don't do anything by themselves, they are just there for convenience).IsEnabledandIsDisabled- self-explanatory.IsandIsNotmethods. Use these when coding your ability to check against the current state type (or useCurrentStateand theiskeyword directly - these methods are just shortcuts).
Classes inheriting from StateBase also have access to:
IsCurrent- self-explanatory.
You can override Log, LogError and ProcessLog to customize the logging behaviour.
If you need to access component information from outside a component, you can use methods in the static ComponentBase.Global class, such as ListComponents<T>(TEntity entity), GetCurrentStateSwitchData(TEntity entity) GetLastStateSwitchData(TEntity entity). The state switch data objects contain some information such as Initiator - the component that called the SwitchState/QueueStateSwitch method and the actual previous/new state objects. You can also use the provided Entity extension methods.
Sometimes you will have bits of data that need to be sent or shared between components. There are several approaches to this:
- Put the shared properties in the
MyCoolEntityscript. This will make them accessible to all components viathis.Entity.<property name>and matches how things are done in Godot at the moment. For example, if your entity script extendsCharacterBody3D, you will have aVelocityproperty inherited from it. It makes sense to add, let's say, aLookAngleproperty to be used by camera and movement-related components. - When some data is only relevant to a select few components, it might make more sense to put it in a custom resource class, and then assign the same resource instance to all components in the editor. By leveraging Godot's resource system we basically get automatic data syncing between components. For example, an
Interactability andInteractingstate might shareInteractDataresource. When theInteractability detects an interactable object and a key is pressed, it will update the properties inside theInteractDataresource with necessary info about this interactable object, then switch state toInteracting. On enter,Interactingstate will read these properties from the resource and then, let's say, attach the referenced object to the player and play the best animation for the given object type. - States and abilities can use the provided
CurrentState,GetState,GetAbilitymethods to directly access other components and modify their properties. While there is technically nothing wrong with this, it is a somewhat dirty approach and generally shouldn't be used.
Mixins is a feature of this library which allows you to reuse code between states and/or abilities without relying on inheritance. In order to create a mixin, simply create an interface IMyCoolMixin with IMixin<MyCoolEntity> as a base interface and implement it on your components where needed.
Inside the methods of the mixin type you will have access to:
Thisproperty, which returnsthiscast toComponentBase<TEntity>.Entityproperty, which is literally the same thing asComponentBase.Entity.- Hooks.
Hooks allow you to run the mixin code before or after the supported component methods. In order to create a hook, create an non-static, non-generic, non-abstract method inside your mixin interface with a default implementation, no parameters and void return type. Attach the MixinHookAttribute to it and specify when the hook should run using the HookEvent and HookType properties. If the HookType property is not specified the behaviour is considered undefined (currently defaults to Post-hook).
If you need to access your mixin's members from inside the component, you can either cast this to the mixin type (the normal OOP way) or access the automatically generated shortcut property with a matching name. In other words, implementing IMyCoolMixin on a component will generate IMyCoolMixin property (which simply returns this cast to IMyCoolMixin).
This library is available as a NuGet package.
However, there is a nuance. Because the provided base classes inherit from Node, the source files of the library must be present in the project directory - otherwise Godot's source generators will not pick them up and the classes will be unusable. To solve this problem the package is split into 2 subprojects. The 'main' project, GodotEntityController, actually contains a source generator, which will extract the library files into your project directory (yes, I am effectively using a source generator as an installation script). The actual library files are located in the GodotEntityController.Internal project. They are referenced in the source generator project as resources.
The library files will be placed in the addons/godot_entity_controller directory inside your project folder. Additionally, a plugin_version file will be created inside. The source generator will re-create the library files if the library directory is deleted or the version string inside plugin_version is different from the current package version (meaning the files will be automatically updated when a new package version is installed).
Therefore if you need to make changes to the files you should not modify the extracted library files and instead clone the repository, edit the code and build your own package.
You might think that at this point it would be easier to just make a proper Godot addon and you know what? You are probably right, but I am stubborn and I like NuGet so I did what I did and this is what we have now.
