Hello! I recently decided to start making a sort of RPG battle engine. The core ideas are that it's real-time combat inspired by MMOs, where you cast actions with costs and cooldowns from a hotbar, except without movement and with multiple controlled characters at once. I'm also aiming to make the battle engine specifically isolated from the other parts of the code, following something that should loosely resemble an MVC pattern?

So far, I've been trying to figure out how I'm going to structure it and I may need advice on how. Attached is my first draft.

https://lensdump.com/i/d8tRpm

The 'Battle' node is the engine. There's a 'Party' node for each side of the battle (should just be Enemy and Player, for now) and a 'Fighter' node for every character fighting.

  • 'Battle' handles most of the processing while 'Party' and 'Fighter' mainly store data; when the player tries to use an action, 'Battle' receives a signal with the action, caster and target, checks the caster's Fighter node to see if they can cast it (whether or not the action is available to them, if they have mana, if it's on cooldown), checks the target's Fighter node for anything that would prevent it (untargetable?) and if not, processes the effect of the action. When damage is dealt to a fighter or if they die, a method is called on Fighter that processes damage and calls a signal to check for effects (and display FlyText).
  • All actions are stored within the action_list resource while action specifically available to a Fighter are stored in their build resource, inside character_sheet.
  • The Party node contains anything that relates to an entire party, whereas the Field node contains anything that relates to every fighter on the field (field effects).

The View node groups visual elements together, and CharacterVisuals specifically groups FighterSprite nodes. Animations are triggered by the signals set off by the Battle engine (though I'm not planning to make any yet).

The Controls (probably a bad name) node groups UI and control-related nodes. It's hazy, but one way could be to have several Buttons placed relative to Hotbar, spawned from the hotbar_setup resource, that trigger the aforementioned signals when pressed. Then FlyText nodes display when damage is dealt or healed, and ParameterBar nodes are directly bound to each Fighter's HP and MP and displayed relative to their sprite's position.

That's the gist of it, but I'm a bit uncertain. First off, I'm worried that I'm using too many nodes. Some of these nodes seem like they may not end up having many methods or even properties, and I'm not sure if that's okay? At what point is it unnecessary to make something a node? Are the nodes I'm using to keep different components grouped, like View and Controls, okay, or should I just leave their children at the root? And should something like the Fighter node be a plain Object-derived class instead ?
Also, I'm wondering about how I should represent actions. What I know is that they should have a name, a description, a cost, a cooldown type (global vs individual), a cooldown duration, a targeting type
(single or multi), maybe an attack type, and most importantly, effects. To give an action complex effects, I need it to carry a method; so does that mean every action should be a class, or an instance of one? One thing I am thinking is that if an action can be an Object, then it would not be difficult to implement actions with cast times, where the Object lives for as long as the casting is going on and calls its effect method if it's completed then is freed, or is just freed if it's cancelled. But that doesn't seem super clean and seems bad memory-wise...?

Could anyone give me some opinions on how this could be improved and how the gaps could be filled here?

The Controls (probably a bad name) node groups UI and control-related nodes.

I usually call this kind of node HUD or UI.

For the hotbar you want to use a suitable BoxContainer node; unless you are using animations for button placement.

Overall the design sounds sane and you probably don't need to worry about too many nodes. However there is one thing I would do differently. The battle branch doesn't make a whole lot of sense as nodes IMO. You might be better off with implementing it as RefCounted (or Reference in Godot 3).

    Zini Thank you for responding!

    BoxContainer sounds a lot better than plain Control, but I'm not sure I understand RefCounted's purpose here; it's likely that all the nodes/objects in the Battle branch will be freed at the same time anyway (unless I have an Action object or node or some other temporary things that would be freed upon finishing their work). Wouldn't a reference counter be superfluous?

    If you really don't need any object beyond the lifetime of the battle scene then technically a node approach would work. From your description I kinda guessed that things like party and fighter models would be persistent.

    Using a hierarchy based on RefCounted still seems to be a cleaner approach to represent the model and also a more lightweight one, if that is still your concern.

    In my game I am doing the following:

    • in case of an encounter (World map, dungeon, whatever) I create the appropriate battle object, configure it with party, enemies and location and then store it in my global world_state autoload object (this is model)
    • switch to the battle scene
    • the battle scene grabs the battle object, sets up the location accordingly and spawns the combatant nodes/scenes, configured with references to the respective character/creature objects from the battle object (this is the view)

      Zini

      From your description I kinda guessed that things like party and fighter models would be persistent.

      Right, that's fair. The way I saw it, persistent data about characters is held inside the character_sheet (if there is any), and the character_sheets of party members... are referred to in an autoload, likely? I'll admit, my only plan was to make the battle engine, so I didn't put any thought into how the battle would start...

      Thank you for the advice! I'll rethink what the Battle branch inherits from. If I don't use RefCounted, I may end up using Object instead.

      • xyz replied to this.

        Trobador With addition of static class properties In 4.x, autoloads are kind of made obsolete (for most cases previously used). You can just write a static class that holds the data, and access it via class namespace without instancing anything.

        Better to use RefCounted than Object btw. Much less bug prone.