Search code examples
godotgdscriptgodot4

How to implement a composable character/skill system in the Godot 4.0 Game Engine?


I am currently experimenting with a prototype for a MOBA style game using Godot.

I am struggling to figure out a way to manage the characters and their skills.

All characters will have similar attributes (Name, health, run speed, strength and so on). However, the skills will be different for all characters (although some will be pretty similar, for instance projectile based skills would have things like: distance, speed, texture/vfx) that should be configurable but allow for re-usable behaviors when possible.

I am not sure how to build something that will allow for simple maintenance/programming.

I tried to use resources for the skills and characters. However, it looks like resources might not be the way to go since I can't seem to have different behavior/scripts for each resource instance.

I.E: Skill Resource

Name: Frost Arrow Speed: 20 Texture: frostarrow.png

However, I don't think I can attach code to it, or if so, I haven't found out how.

I also tried with a PackedScene. While I can have custom code using a scene, I can't figure out how to access the skill name from the packed scene.

And then, for the Character. I think for this one, I can use a Resource. I would have a Name, Sprites/3D model and so on. I would like to have a way to assign skills to my characters.

And from my player controller, be able to access those skills to display them on screen and call them when needed.


Solution

  • For the purposes of this answer:

    • "thing" refers to characters, weapons and projectiles.
    • "wraps" refers to a composition relationships, when I say that an object wraps another it means that it has it as a component.

    The general answer is that will use resources. To be more specific you will use custom resources plus scenes.


    The scene stack

    I'll start with the scene stack because is the clearer part of this. These are all scenes that exist in the game source:

    • Asset: Created in another software, has meshes, sprites, materials, shaders, skeletons, animation, etc.
    • Prop/Skin: Wraps the asset. Adds colliders, bone attachments, and any other auxiliary nodes.
    • Agent/Character: Wraps the prop. Adds behavior (scripts). The character controller is one of these.

    Now, if you want to change how it behaves you need a different agent. If you want to change how it looks you want a different prop.

    So there can be multiple things with the same behavior but different appearance (given by the prop), and there can be multiple things with the same appearance but different behavior (given by the agent).


    The relationship between scenes and custom resources

    However, what I described above exist in the scene tree. Things do not always exist in the scene tree (sometimes they exist in menus or inventory-ish containers). There you need custom resources.

    There will be two kind of custom resource classes, and I'll get back to that... What I say here can apply to both.

    The custom resource classes:

    • Have properties for the data you need about the thing.
    • Have PackedScenes properties, they can instantiate.
    • Have a method to instantiate the PackedScenes, which also sets a property of the instance to the custom resource instance itself (this is a form of dependency injection and inversion of control).

    So there can be multiple things with the same behavior but different configuration (given by the custom resource).


    Sometimes the custom resource will reference multiple PackedScenes. Here are a few cases:

    • The custom resource can also include a PackedScene to instantiate as Prop/Skin inside of the Character/Agent.
    • Sometimes you want multiple options to instantiate a thing in the scene tree. For example, you might have a different PackedScene to instantiate a weapon when equipped, than when dropped in the world.
    • You might have an Agent/Character scene you use when a character is being controlled by the player, and another when it is a bot, despite them being conceptually the same.

    It is better if you don't think the scene is the thing. The custom resource is the thing, and the scene is just a wrapper you use when it needs to exist in the scene tree... And you might use different wrappers for different occasions.

    Of course, if some of the wrappers are generic, you don't need to have a property you configure in the custom resource. Similarly, the custom resources do not need to have methods to instantiate every kind of wrapper scene... Just do the ones you actually need.


    The custom resource stack

    In practice you will likely need two kinds of custom resource classes:

    • General resource. This has all the data that is common between all things of the same kind.
    • Specific resource. Wraps a general resource, and has all the data needed for a specific thing.

    For example one general resource might represent a weapon model, and it has stuff like ammo capacity. But there can be multiple weapons of the same model in the game. The specific resource represent one of those weapons, and it has stuff like the amount of ammo it currently has.

    You will want the general resource to have the PackedScene of the Agent/Character, because they all behave the same. But the specific resource to have the PackedScene of the Prop/Skin because each one might be customized to look different.

    Also, the custom resource you want to inject into the scene instance is the specific resource. This way the scene has access to all the data (since the specific resource has a reference to the general resource), and it can take control of specific data (e.g. it will update its ammo count). You proably don't want to change the data of the general resource in runtime.

    Furthermore, the general resources would probably exist as resource files in the source of the game. But the specific are created in runtime (and you might save their data when saving the game, and of course recreate them when loading the game). In fact, you might want to include a method in the general resource script to create instances of the specific resource.