Skip to content

Metal-666/GodotEntityController

Repository files navigation

GodotEntityController

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.

Architecture

Design

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:

Godot example scene tree screenshot

  • Entity: CharacterBody2D

  • States

    • Idling

      Does not contain any logic.

    • Moving

      Uses the provided movement input to calculate the player's speed and sets the Velocity property. If the movement input is 0, switches to Idling. If no floor is detected under the player, switches to Falling.

    • Jumping

      On 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.

    • Falling

      Accelerates the player downward. When a floor is detected under the player, switches the state to Idling.

  • Abilities

    • ReceiveInput

      Uses the Input class or overrides _*Input methods to read user input. Forwards the input to other Abilities by setting properties (or calling methods) on them.

    • Move

      When the necessary conditions are met (the player is Idling and the received movement input is not 0) switches the state to Moving.

    • Jump

      When the necessary conditions are met (the player is either Idling or Moving and the jump input is pressed) switches the state to Jumping.

    • ControlCamera

      Updates 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.

Code

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 MyCoolEntity script derived from Node and add the IEntity interface.
  • Create abstract MyCoolEntityStateBase and/or MyCoolEntityAbilityBase classes by extending StateBase<> or AbilityBase<>; the generic parameter will be the MyCoolEntity type.
  • 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 IsEnabledByDefault property to true.

API

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):

  1. CurrentStateSwitchData value is updated.
  2. Exit is called on the current state.
  3. Current state is removed from the tree.
  4. New state is added to the tree.
  5. Enter is called on the new state.
  6. CurrentStateSwitchData value is cleared.
  7. LastStateSwitchData value 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.
  • EnabledAbilities and DisabledAbilities - get a filtered list of abilities.
  • GetState<TState>(), GetAbility<TAbility>() - get state/ability of the specified type.
  • Can<TAbility>() - returns true if 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:

  • IsEnabledByDefault property. Set this to true in the editor on all abilities that should be available to the player when the player scene is loaded.
  • BlacklistedStates and WhitelistedStates properties. Set these in editor and use the IsInBlacklistedState and IsInWhitelistedState getters when coding your ability (these properties don't do anything by themselves, they are just there for convenience).
  • IsEnabled and IsDisabled - self-explanatory.
  • Is and IsNot methods. Use these when coding your ability to check against the current state type (or use CurrentState and the is keyword 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.

Sharing data between components

Sometimes you will have bits of data that need to be sent or shared between components. There are several approaches to this:

  1. Put the shared properties in the MyCoolEntity script. This will make them accessible to all components via this.Entity.<property name> and matches how things are done in Godot at the moment. For example, if your entity script extends CharacterBody3D, you will have a Velocity property inherited from it. It makes sense to add, let's say, a LookAngle property to be used by camera and movement-related components.
  2. 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 Interact ability and Interacting state might share InteractData resource. When the Interact ability detects an interactable object and a key is pressed, it will update the properties inside the InteractData resource with necessary info about this interactable object, then switch state to Interacting. On enter, Interacting state 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.
  3. States and abilities can use the provided CurrentState, GetState, GetAbility methods 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

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:

  • This property, which returns this cast to ComponentBase<TEntity>.
  • Entity property, which is literally the same thing as ComponentBase.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).

Installation

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.

About

A library for creating character controllers.

Topics

Resources

License

Stars

Watchers

Forks

Languages