Designing a Scalable Combat System with Composition

I’ve been designing and just started working on a top-down roguelike game with only a few combat interactions: attack, basic attack, dodge, block and active skills. As you progress through a run, you upgrade your weapon’s properties and attacks and get new active skills. The game is mostly focused on what weapon you have and how you built it. Of course, that’s all the design, what I have made so far was quite literally just movement (walking and dodging) and taking damage.

I’m trying to get a better understanding and practice with composition, so everything I did with the project was based on it (using Mastering Composition for Beginners in Godot and Using Composition to Make More Scalable Games in Godot for reference). Now that I am finally trying to approach combat, I hit a wall. At first, I though the easiest way to go on about it was by making an AttackComponent that checks if an attack was requested (whether through InputComponent for the player or EnemyAIComponent for enemy NPCs) and calls an attack if true, but because, as I mentioned before, the attacks themselves will change based on upgrades, I thought maybe I should make the attacks easier to build, which could be achieved by making components for the attacks, but that means that attacks will be Nodes, and since the main source of variety between the player across runs will be the different attacks paired with different upgrades and properties, that means a LOT of Nodes, which I imagine would not be practical and probably terrible for performance.

I believe combining both systems — having an AttackComponent that checks if the entity can and should attack and calls an attack (based on what attack options the entity has access to), and the attacks themselves being indeed Nodes with their own components for their properties and animations, which are assigned to the AttackComponent — would work, but something feels off about it.

Edit: I had forgotten to mention that I am a beginner. I can code systems at surface level, similar to how one would in a tutorial that has no compromise with future features that may interact with each other, which is exactly why I’m struggling with this.

Tried to figure out a way to approach this and I feel like I’m heading towards a direction that works. Since in my concept the player will start with a weapon and maintain that weapon throughout the whole run, upgrading the weapon itself and the weapon’s inventory (since skills are exclusive to a weapon), each Weapon will be its own scene, with a WeaponStats resource containing all the properties every weapon will need. Attacks will also be scenes and also contain a resource for their stats, along with a script on the attack’s root node that defines its behavior. With the attacks being scenes, I imagine I could componentize upgrades and add them as child nodes of the attack’s scene.

Of course, there’ll probably be more things I’ll add to the Weapon scenes, but so far that’s what I believe is the bare-minimum, but I STILL don’t know how all of that could eventually connect to the entity that will end up using those attacks, AND actually call the attack. The fact that I intend to let the player get new attacks and upgrade attacks during a run doesn’t help at all.

I thought I had watched that first video, and maybe I did, but I was also confusing it with the one in this thread:

Feel free to read my comments on that.

I then talked at length about composition in Godot in this thread:

I’m linking them because it’s a LOT to type.

First Video

This guy kinda gets components, and kinda doesn’t. First of all, his naming SUCKS, but he shares that with the second guy. You don’t name something “Component”. You just name it what it is. Sprite2D isn’t called Sprite2DComponent, so why should yours be called that?

Also, an Input component seems like a good idea, but it actually breaks the Single Responsibility Principle that he’s talking about while he’s showing off his components. In fact, SRP is really hard to implement well, and when you do it looks dumb. Using SRP would mean you have one object for left input, another for right input, and well… you get the idea.

Instead, input should be handled by the object that is affected by it. Or, by a component in some cases. But not an Input component. Here’s why: That Input component doesn’t DO anything. For the Movement component to work, it has to rely on the Input component existing. This is called tight coupling, and again, goes against the SRP he says he’s following. (The one exception to this is if you’re using a Command Pattern.)

I stopped about 5 minutes in, but overall, he was discussing the benefits of components well. I just don’t feel like watching an hour of video.

Second Video

In the second video, his intro is good, but his naming convention SUCKS. Notice how everything he made was called HealthComponent and HurtBoxComponent. Right after he got gone explaining how Sprite2D and AudioStreamPlayer are components.

In this post, I walk you through creating a Health component:

It even has a cool icon:
image

And a number of visual representations for the same component:

Your Stuff

I would say the Resource approach you’ve adopted is the best one. I walk someone how to implement something like that in this thread here:

For your needs, basically you are using a Command Pattern. So if you keep all the details inside your Resource object, you can just attach them to the Weapon. Those Resources can even have generic functions in them like attack() or use(). Then you can just call those functions and have custom code when necessary.

I’m sure you’ve been told that many times, but WOW was that a very resourceful reply! Very informative and eye-opening!

I did forget to include in my post that I’m not exactly an experienced developer and I have only a single Asteroids clone as a finished project. If anything, I’d say I’m barely crossing over from a beginner to intermediate, and that’s probably a stretch. I admit plenty of the stuff you linked was way too advanced for my understanding as of now.

Based on the code the author of A discussion about composition and code complexity included, they actually used the first video I linked as reference as well.

I have learned about composition so long ago that, clearly, I had forgotten its purpose. Instinctively, I knew why I’d want components for stuff, but I was completely missing the Single Responsibility Principle you mentioned. I had thought it was weird that the MovementComponent required InputComponent to work in that video’s code, and found it even weirder that they were communicating within the player script, but I hadn’t questioned it since the author of the video clearly has more experience than I do. Thank you for snapping me back to reality on that one.
I will also refrain from naming things “ThingComponent” from now on so I don’t get scolded for it!

Your post on Am I doing Components/composition right? was by itself far, far more helpful than every single video I have watched on the subject so far. You have also cleared my confusion about “call down, signal up” in there, too.

Although I don’t really see how I’m using the Command Pattern as you mentioned (and would love to find out how you identified that!), I’ll target this subject and follow through with the Resource approach! I did believe it was heading towards the right direction and you did say it was the best one, so I’ll capitalize on that. The post you linked with that was extremely helpful already, after all!

I’m sure I’m not going to immediately figure it out — in fact, even with all your help I still can’t really visualize how to make this scalable —, but at least you pointed me towards a concrete direction by pointing out the Command Pattern and how that could work with Resources. I’ll probably stop trying to approach everything as components and leave them for the upgrades as it sounds the most appropriate use for them in the context of my project.

Once again, thank you!

Glad I could help. Feel free to ask more questions if you’ve got them. I know I gave you a lot to read.

This is the Command Pattern. Your Attack component has an interface (most likely functions) that both Input and EnemyAI connect to, to tell it what to do. That’s the basis for the Command Pattern. For it to truly be the Command Pattern, you would store all those inputs in an Array (or more likely a Linked List - which is not a data structure Godot has so you’d have to make it) and then you could rewind or replay those actions.

Games that do this often use it to rewind actions, like in Braid, or have another entity follow you mirroring you at a delayed time, or for games where you want to replay everything. Like recording a turn-based game so you can play against yourself for example. It’s also how multiple undos and redos work in the Godot Editor.

A while have passed, but I’m still working on this. I have to admit attempting to put the Command pattern into practice here did not go well, and I wasted about two weeks even trying to understand how to make use of the Command pattern on Godot with GDScript mostly using the Game Programming Patterns page of it as reference since all the tutorials and posts about it I’ve seen had very different approaches. Blaming myself on this one, perhaps I’m a little too much of a beginner to understand that stuff as of now.

Now, back to the combat system, I believe I have found myself facing the right direction for the first time thanks to your post in How to approach modifiable item stats and your response to my post. Hopefully my explanation of what I came up will make sense:

I have decided to isolate the Player, Weapon and Attacks completely. Rather than “making the player use an attack”, I started thinking of “making the weapon attack” instead. So, I went back to the planning process of how I want this feature to work.

  • Different weapon types
  • Attacks are equipped in the weapon
  • Certain attacks are only available to certain weapon types
  • Attacks have different types (basic attack, heavy attack, skill) which means they can only be equipped in that respective slot in the Weapon inventory
  • Attacks have their own animation using the AnimationPlayer so I can load and play when that attack is called regardless of the weapon (I will have to find a way around inconsistency caused by different sprite sizes and hitboxes)

Keeping those things in mind, I made a Weapon Resource and a RefCounted Enum for the weapon types.

# base_weapon.gd
class_name Weapon extends Resource

@export var base_damage: float = 5
@export var swing_speed: float = 1
@export var equipped_attacks: Array = []
@export var weapon_type: WeaponTypes.Type

func attack(attack_requested):
	pass
# weapon_types.gd
class_name WeaponTypes

enum Type {
	AXE,
	DAGGER,
	SPEAR,
	STAFF,
	SWORD,
	HAMMER
}

The logic for attacking will be handled in the Weapon’s attack() function, which checks which Attack stored in the equipped_attacks Array was requested and “casts” that attack.

Then, what I am considering is creating a WieldedWeapon Node under the Player scene, and that WieldedWeapon Node will be responsible for the player’s physical weapon (i.e. its sprite, hitbox and being where the attack animations are played), handling the logic for equipping new attacks (which will be then stored in the Weapon’s equipped_attacks Array) and damage calculation based on the weapon, the attack, and the player’s stats and equipment.

Of course, there are still a few gaps I can see in this design. For example, I’m unsure of how to even begin with the WieldedWeapon having its own sprite and hitbox since each run means starting over with a random weapon, and of course a dagger and a spear would have different visuals and hitboxes.
I’m also not satisfied with the naming; I’m using “WeaponTypes” because “Weapon” is already the name of the resource with all the weapon stats, which leads to the terrible @export var weapon_type: WeaponTypes.Type line.
I don’t have a clue at how to approach attack animations in general for this, specially since I’d have to animate the attack and somehow pass that animation to the WieldedWeapon Node.

There are obviously other issues with this design that I’m too ignorant to spot, but I believe that’s a solid starting point. At least for now, I can settle with manually setting the WieldedWeapon’s sprite and hitbox to get the system running and worry about figuring out how to make things dynamic. At least my project won’t be frozen in space any longer.

So there’s an easy solve for the first problem you spotted, which was the weird namespace issue. That’s sovled by combining the two files. Now, when outside the class, you just refer to Weapon.Type which is much more readable.

Second, since you’re not implementing the attack() function, make it and the class @abstract. This will make it so you have to implement the class to get it working. And you don’t need a pass function body.

@abstract
class_name Weapon extends Resource

enum Type {
	AXE,
	DAGGER,
	SPEAR,
	STAFF,
	SWORD,
	HAMMER
}

@export var base_damage: float = 5
@export var swing_speed: float = 1
@export var equipped_attacks: Array = []
@export var weapon_type: Type

@abstract
func attack(attack_requested)

I’d recommend creating a single weapon first with what you’ve got. Then implement a second. Then see what you can move to the Weapon resources. As you code it, things will work out.