How to create an architecture for various different attack scenes

Godot Version

4.5 stable

Question

I’m having trouble creating the architecture for different kinds of attacks in my game.

I’m making a 2D platformer. Characters can equip different Weapons, which are RefCounteds that contain data like the amount of damage they do. The reason they are RefCounteds, not Resources, is because weapons of the same type can have unique stats, and a base Resource called WeaponData is used to create and randomize them.

Finally, each weapon has a PackedScene exported to it, which is used to actually spawn the attack scene in the game world. This is because each kind of attack can have wildly different behavior. The scene should be given the data from the RefCounted Weapon class when it spawns.

So this is basically what this looks like:

class_name WeaponData extends Resource

@export var damage: float
@export var attack_scene: PackedScene

func create_weapon() -> Weapon:
     var new_weapon = Weapon.new()
     #Randomize damage here based on the exported var
     new_weapon.damage = randomized_damage
     return new_weapon
class_name Weapon extends RefCounted

var damage: float
var attack_scene: PackedScene

When a character actually uses an attack, they will instantiate the scene, then give it the data it needs.

This is the part where I’m stuck. I first made a base “Attack” class that extends Node2D, since I figured that every Attack scene should have some common properties and functions, like set_initial_position() or calculate_damage(), or a variable for the character who spawned the attack, etc.

But that doesn’t seem like it will work, because not every Attack will want a Node2D to be the root. For example, maybe some attacks have a CharacterBody2D as the root which needs to move_and_slide(). If a Node2D were the root, it wouldn’t move with the body, which could cause issues. It feels like a better idea to make every Attack with whatever root node works best for it. Yes?

But if I don’t share a common root node class for all Attack scenes, how can I pass the data from any Weapon to any Attack safely?

Should I be extending my Weapon class to create specific Weapons for each Attack?

Like this:

@abstract class_name Weapon extends RefCounted

var damage: float
var attack_scene: PackedScene

@abstract func instantiate_attack_scene(source_character: Character, target: Character)

func calculate_damage(source_character: Character):
     //Some logic here for calculating base damage shared between all Weapons
class_name Explosion extends Weapon

func instantiate_attack_scene(source_character: Character, target: Character)
     var scene = attack_scene.instantiate()
     if scene is ExplosionAttack:
          //Set up the scene however it needs to be set up, like by setting its damage and position...?

I’m not sure about this… it still feels off to me somehow. Does anyone have a similar system or some insight?

Get rid of WeaponData and Weapon. Don’t premeditate the system. Implement several weapons separately. Once you have that, look for shared abstractions. If there are any - extract them. Repeat until you have all weapons implemented. Design from specific to general, not the other way around. Otherwise you risk falling into traps of analysis-paralysis and overdesign. Those can waste ridiculous amounts of your time… and sanity.

3 Likes

Well, the issue here is that I’ve already made this game on a smaller scale in a different engine, so I know most of the abstractions. Every character needs a variable to hold their current weapon. Weapons can be changed out with other ones from the inventory. Weapons can be dropped from enemies and have randomized stats. All weapons share some common properties. There are three inherited classes from Weapons: Melee, Projectile, and Summon, which each define some unique properties.

Thinking back on it, the exact way that I accomplished this before was by doing the last thing I said in this post. If I wanted to make a new kind of projectile, I would inherit Projectile and call it something like Bullet. Then I would create a separate Bullet “scene” in Godot terms, and give it to the Bullet class. When attacking, a character would read from its current Weapon to instantiate the “scene” that was in the Bullet class. The Bullet scene would then set itself up entirely independently from the Bullet Weapon.

But in that engine, “scenes” didn’t work the same way. They were basically just Godot RefCounteds. I was able to create a base “Attack”, then inherit a MeleeAttack, ProjectileAttack, and SummonAttack, then inherit those further depending on the specific attack. So for example, I’d make a BulletAttack. Then all Attacks shared some common properties and functions.

But in Godot, we have “scenes” which have a specific type of root node. So the question now is just where to put those common properties and functions in Godot, and whether I can even use the same type of root node for all Attacks. I can’t really have a base “Attack” class that inherits from some kind of Node to use in all my scenes. Or can I?

I think optimally, in my case, it would be nice to have a base Attack class somehow, which inherits from some kind of Node, like Node2D. But as I was creating another Attack today that used a CharacterBody2D as a child, I realized that the parent Node2D would not move with the CharacterBody2D, so it would be impossible(?) to get the position of the Attack without knowing what kind of Attack it was specifically.

Unless… I could just make the CharacterBody top level… and then have the Node2D follow its position. Edit: But then making it top level comes with other problems…

I might have just been asking the completely wrong question here. Maybe I just should have asked how to make a Node2D root node move with a child CharacterBody2D.

You’re getting confused because you’re insisting on building your class hierarchy from general to specific. This is almost always bound to fail in one way or another.

Do it other way around and your final system will be the result of multiple passes of abstraction extraction that will fit into Godot’s technical hierarchy you’re bound to inherit from. Don’t get too obsessive over DRY. Some repetition is perfectly fine. It’s an acceptable price to pay for not getting insane while trying to shoehorn problems into idealized premeditated hierarchies.

What’s the point of having broad base classes like “Attack” when they cause more problems than they solve?

Also, when inheritance cannot solve your problems, try using composition.

2 Likes

Alright, so let me try to work from specific to general here. I’m just confused how exactly I would even do that in my case or how it would help me with this particular problem.

I’ve got my characters and level and everything all set up. Now it’s time to start giving them a way to attack each other.

So what’s the first step? Well, let’s start super specific. Let’s just make a bullet scene for them to spawn.

I go ahead and make a bullet scene. Maybe the root node looks like this:

class_name Bullet extends Area2D

var damage := 5.0
var speed := 100.0

#some functions for what happens when it collides with a character, etc.

Alright, then my characters need a way to spawn Bullets. So I give them a variable that holds the Bullet PackedScene. Then I make them spawn it and shoot at each other and it works.

Cool, done. So then I ask myself, what’s next? Well, I want characters to be able to switch weapons, so let’s make another scene for another attack. I’ll repeat some code and do something like this:

class_name Arrow extends CharacterBody2D

var damage := 10.0
var speed := 200.0 

#some functions for what happens when it collides with a character, etc.

Now, my characters should have a generic “var attack: PackedScene” variable instead, and then I can write some logic for them to switch between Bullets and Arrows. All they do is instantiate their attack scene, and it sets itself up on its own, since both of them have their damage just predefined in their scripts.

Okay, what’s the next step? Well, I need the damage dealt to take the character’s Strength stat into account. It should be the attack’s base damage + Strength. So now I need some way for the characters to give that info to the scenes, or for the scenes to get that info.

But of course, it wouldn’t be type safe to do something like this in the Character script:

func create_attack():
     var atk = attack.instantiate()
     atk.damage += strength

So now I need some kind of abstraction. If the character knew what class it was instantiating, we could give it whatever info it needs. And, well, I can see a very clear pattern between both scenes. They both have a damage and speed property. They even both have some identical functions. Maybe the only way they’re different is how they fly through the air. So I guess you could create an abstraction here and call these Projectiles.

But how do we actually represent this abstraction in code? I can’t have Bullet and Arrow both inherit from some class called Projectile, since Bullet inherits from Area2D, and Arrow inherits from CharacterBody2D.

What’s the next step here?

Ya what normalized has said rings very true.

It is in general easier to just create the feature the best you can at first then try and plan around it. Everyone codes differently so it would be easier to plan around your own coding than trying to follow someone else’s template or try to plan ahead of your own code.

I fell into the trap of planning around what I would need and never actually got to making the feature so It would just sit in Limbo forever and when I actually finished the framework and used it, it was always overkill and needed refactoring anyways because I needed it to function a slightly different way.

Just code my friend, just code.

1 Like

The thing you actually “need” is shared functionality. Abstracting to a base class is not the only way to extract shared functionality.

Those two classes cannot share common functionality via a custom base class because they already inherit from different branches of Godot’s native technical hierarchy (which is immutable). So inserting the Projectile base class is not feasible, even though it looks “logical” from your game semantics perspective. However, your game semantics is not the thing that dictates your class hierarchy. Godot’s node class hierarchy is. So your hierarchy needs to conform to it. Unless your hierarchy is completely detached from engine’s hierarchy.

The “solution” is to simply live with some repetition or have both classes reference an object that handles the shared functionality, aka composition.

Note that inserting the Projectile base class would be a viable option if both of your classes inherited from the same native class. That’s why it’s important to start building from specific, so you can determine which base classes are feasible and which aren’t.

As for initialization, I don’t see how abstracting to a base class would make things different here. Your code example wouldn’t be type safe even if all attacks shared the same base class as PackedScene::instantiate() return type is always Node.

You can simply establish a convention that every attack class needs to implement a setup function that takes in the character reference. For additional safety you can check or assert if that function is implemented.

var atk = attack.instantiate()
assert(atk.has_method("wielded_by"))
atk.wielded_by(self)

Okay, awesome, thank you, this makes sense to me. But about this:

As for initialization, I don’t see how abstracting to a base class would make things different here. Your code example wouldn’t be type safe even if all attacks shared the same base class as PackedScene::instantiate() return type is always Node.

Of course, you would just edit the code a little bit.

var atk = attack_scene.instantiate() as Projectile

Or however else you would want to do it. But that’s not relevant anymore anyway, since we’re not making some base Projectile class.

Let’s just continue with what you said. You gave two options, from what I can tell, which are:

have both classes reference an object that handles the shared functionality, aka composition.

or

every attack class needs to implement a setup function that takes in the character reference. For additional safety you can check or assert if that function is implemented.

And of course, I’m sure you can do both of these things at the same time.

The second option might work for this specific thing, but in my case, I already know it will end up not working for other specific problems or getting messy once the system is a bit more fleshed out, because I’ve already gotten to that point before. I’m just sort of trying to walk through simplified examples with you so I can understand where I’m tripping up.

To get to where I’m at, I’m actually already using composition like you said in the first option, in a way. Having both classes reference another object that handles the shared functionality is exactly the sort of thing that feels like it should work for me, and I was trying to figure out how to actually achieve that composition and getting stuck.

So, now that you’ve helped me go from specific to general, I think I might be starting to see a solution. We can create a ProjectileData custom Resource that handles the shared functionality, like the damage, speed, and some functions. Why a Resource? Well, just so we can export values like the damage to adjust them in the editor and make different weapons. So we end up simplifying our Bullet and Arrow classes substantially:

class_name Bullet extends Area2D

@export var projectile_data: ProjectileData

#Bullet-specific stuff goes here, like what it does in physics process

Arrow would be almost identical. So now I could just open the scenes for each one to easily edit their properties in the exported ProjectileData, in the editor. Very simple, right? Is this the right track?

Let’s say ProjectileData implements that setup function you suggested, which takes the character reference. Or… that wouldn’t work, would it? Because when the Character instantiates the scene, it asserts has_method(), but that would not be in the scene’s class anymore. It would be in ProjectileData, because I want all the shared functionality in some other class.

So instead… I ended up making the character have a variable for ProjectileData, not for a PackedScene. This way, a character knows exactly what it has and it’s 100% type safe. When you swap weapons, you just swap what ProjectileData you have.

So where does the PackedScene go? Well, inside the ProjectileData. Then ProjectileData just has its own specific function for instantiating the scenes. The character can just call that function. So combining it with what you’ve given me, it would look like this:

Character class:

func attack():
     projectile_data.spawn_projectile(self)

ProjectileData class:

@export var damage := 5.0
@export var speed := 100.0
@export var attack_scene: PackedScene

var current_character: Character

func spawn_projectile(source_character):
     current_character = source_character
     var atk = attack_scene.instantiate()
     assert(atk.has_method("setup"))
     atk.setup(self)

This is almost exactly what I currently have, just heavily simplified, and now a bit improved with the help you’ve given me so far. Or, well, I’m actually using RefCounteds that are created with the Resources since each weapon is unique. The code above is probably stupid, lol. Just take it as a general concept.

So at this point, I have two things to ask:

  1. This is on the right track according to you, yes? I was already using composition in this way, but I guess I got stuck trying to use inheritance with the scenes. The has_method() function on its own seems to solve the scene issue.
  2. Is… is has_method() really okay to use? Doesn’t it just… smell? I’m just gonna be up front, it feels really bad using a string to check for a method, and also forgoing autocomplete and errors for the arguments. Feels really frail, and unwieldy to debug and code with, and will probably just get messy later. But if all my attack scenes cannot have a common root node… I don’t really see another solution.

Edit: Sorry, I really butchered the last code examples when I initially posted them and left them for a while before I caught it. It still might not be functioning code, lol.

Sounds too convoluted.

Btw GDScript is heavily string based. Accessing properties or methods via strings is fine.

Again, you’re trying to abstract before implementation. Two shared properties really don’t require functionality sharing. If that’s “just hypothetical” and in reality there will be more, then that’s precisely the problem I’m talking about. Don’t rely on hypotheticals. Like, at all.

Actually implement whole Arrow and Bullet classes and then see how much of their functionality overlaps. Make decisions on the basis of implementations.

If you have only two classes, then use if to discern between them upon instantiation. It may not be good for a system but you haven’t got a system (yet). You only have two classes. Use solutions that are optimal for what you currently have. Don’t architect until you absolutely must. And even then, architect in tiny increments.

1 Like

Don’t worry about code smell… That is something corporate game studios do to find underlying issues.

Also when I implement features I don’t think about the structure of the code. I only think about how can I make this work at all in my current code base. When I feel the feature is working and I can change the values I want easily if you have time then go through and fix up your code base, so that next time you come back you can append a new feature or change how something works.

It sounds like you want to create structure in the codebase from the start but you need to create structure in your gameplay first before you can structure your codebase. The gameplay makes the form of the code not the other way around.

In general I’d do it like this. Make each attack a configurable scene. Cover as much variety as you can with configuration parameters. So, depending on your gameplay, arrow and bullet may be the same scene or may be two different scenes. No class hierarchy of attack scripts, no abstract attacks. If similarities do appear, look if two similar attack scenes can be made into a single scene that can be configured differently. If not, keep two separate scenes. If a chance for class inheritance appears anywhere in the process of implementing various attacks - take it, but don’t design the system around class inheritance.

Note here that Godot has scene inheritance as well, which is a different thing from script class inheritance.

When you instantiate the attack scene, make a pact with yourself that this scene, whatever it is, always needs to execute setup(character) immediately upon instantiation. What the scene will do with this - it’s its internal business.

1 Like

Again, you’re trying to abstract before implementation. Two shared properties really don’t require functionality sharing. If that’s “just hypothetical” and in reality there will be more, then that’s precisely the problem I’m talking about. Don’t rely on hypotheticals. Like, at all.
Actually implement whole Arrow and Bullet classes and then see how much of their functionality overlaps. Make decisions on the basis of implementations.
If you have only two classes, then use if to discern between them upon instantiation. It may not be good for a system but you haven’t got a system (yet). You only have two classes. Use solutions that are optimal for what you currently have. Don’t architect until you absolutely must. And even then, architect in tiny increments.

So I’d just like to clarify again that I’ve already made this entire system once with dozens of different attacks in a finished game demo in a different engine. My examples have just been overly simplified versions of that so I don’t have to explain my entire game in precise detail. I know exactly what properties and functions all the different things will have and what overlap they will have, but I just need to figure out where to put them, since this is a different engine with a different framework. But I understand what you’re saying here, of course. I have not implemented it in Godot yet, so I should implement it in Godot first and foremost.

And, well, I did. I’ve been implementing it, and it has been working fine, until I went to create a different Attack I hadn’t created before, which wouldn’t work the way I had been doing things. So I learned that my existing framework would need to be reworked.

Of course, I’m not a master programmer either; I’ve only been doing this for a little over a year and am self-taught. So I totally need some more guidance, and I appreciate that you point out that the system in general could be convoluted. It’s not like it was perfect the first time I made it. It was probably horrendous, but it worked. So it could stand to be reworked from the ground up.

I just don’t want it to seem like I’m just doing nothing but scratching my head thinking and not doing any coding. I’ve done a ton of implementation and testing and have a framework that I’m trying to fix, basically. I arrived at all of my current abstractions through trial and error and finding patterns as I made the game the first time, and as I’m making it this time.

So, can we first perhaps start with the assumption that characters must have a weapon variable for storing their current weapon’s data? Like min damage, max damage, cooldown, range, a boolean for whether it’s been buffed, its name, description, inventory sprite, etc… you get the idea. These things need to be accessed by the UI or other objects in lots of places, and they need to be accessed via the character or inventory. So all that data needs to be in one neat little class. Again, I’ve done this before. I want it in one class so I can directly access it like print(character.weapon.description) so I can safely access and manipulate a character’s current weapon’s description or all its other data without knowing anything about what weapon it is.

Let’s maybe just throw all the inheritance out the window for now for the sake of explanation. Start with a base Weapon class that’s just for that data. A Resource would make the most sense so we can configure it in the editor. The only question is how do we actually use the Weapon to instantiate attack scenes?

Well, how about this? I might just be really stupid for not having thought of this until now:

Just make each Weapon know what scene it has. So Weapon has a create_attack() function. Whenever I want a new type of Weapon, just override that function. Entire problem solved.

class Sword extends Weapon

func create_attack():
     var sword = attack_scene.instantiate() as SwordScene
     sword.setup()
     //Do whatever else the sword scene needs

You can even make Weapon and create_attack() abstract so the editor reminds you to implement the function.

Yes, weapon should hold a reference to its attack scene. If the scene is configurable you can also keep a configuration data resource in the weapon, or have a resource class that represents a specific weapon, that holds both; scene and its configuration data. Something similar to what you already described in your previous post, just without excessive inheritance.

If you don’t want to drag configuration data along, you can export the configuration data in the attack script and create an inherited scene for each specific attack and configure it in the editor.

I’d also think about making the weapon and the attack one and the same.

This makes so much sense and would work perfectly for me, thank you! This is the sort of advice I needed. I will try everything you’ve said here.

I have already thought about making the weapon and the attack one and the same, but I couldn’t figure out how that would be possible. I could see a system where you just have the configuration data and the scene without an extra Weapon class, but the problem is that I want to randomly generate unique Weapons using the configuration data.

So the system is just:

  1. Configuration data (let’s call it WeaponData) is used to create different weapon types in the editor
  2. Configuration data creates a unique Weapon RefCounted with random stats based on the configuration data and an attack scene
  3. Character uses the Weapon to generate the attack scene

Unless, of course… I could just have the configuration data resource duplicate and randomize itself. I was just avoiding that because most people who know what they’re talking about seem to say custom Resources should not be modified at runtime and are just static data containers.

Sounds good. Each attack scene will probably need a different configuration class.

In general, there’s nothing wrong with duplicating and using custom resource objects in this way. Duplicating becomes problematic if a resource holds large amounts of data, like meshes or bitmaps.

If it only contains atomic properties and references then it’ll operate the same as a RefCounted, with additional conveniences of automatic property duplication and ability to serialize to disk. If you don’t need those - use plain RefCounted as it’s a bit more lightweight.

Hmmm… I wonder.

Duplicating Resources might make my life a lot easier by eliminating the RefCounted middle man. But I don’t need property duplication or serialization at all, so… RefCounted just makes way more sense for the actual unique weapons.

Each attack scene will indeed need a different configuration class. But there shouldn’t be too many different configuration classes now. I figured out how to flatten everything out a bunch thanks to talking with you.

I’ll have some very light inheritance. But basically every single weapon in the entire game will be able to be created with 3 different classes that inherit from Weapon now. I won’t need to create even further inherited classes for more specific weapons, which I was worrying about before. All the specific stuff can now be done in separate scenes. I did some testing already, and it’s super clean and simple and works! Thank you! I guess I just live with having a data config resource class for each of the 3 weapon classes. 6 classes total is perfectly manageable.

1 Like