How to structure code for switching weapons when each weapon affects the player's behavior?

Godot Version

4.5

Question

I am creating a 2D, top-down action game with gameplay similar to the Devil May Cry series. In this sense, the player has 5 weapons, of which they are freely able to switch between at any time. All the weapons have unique attacks, but can be controlled using the same control scheme. For example, holding the movement directions away from the cursor and attacking results in a unique move that differs depending on the weapon that is being used- if the sword is equipped, then the player will launch an attack in a direction that knocks an enemy airborne, and if daggers are equipped, then the player will dash through the enemy closest to the cursor before unleashing a series of stabs. In a similar vein, if the player holds down the attack button, then a unique charge attack will be carried out, with the sword carrying out a spin attack, while the daggers would release an onslaught of slabs right in front of the player.

With these varying properties and actions, the structure approach I tried was to give the player node a weapons manager node. The player code manages the player’s behaviors and controls, such as moving, jumping, and dashing. The weapons manager holds each of the 5 weapons, which each has the correlating attacks, hitboxes, and attack behaviors. Depending on the inputs the player receives when attacking, the specific move will change (such as hold, or holding back and attacking). The weapon selected is managed in the player code, and will let the weapons manager know which hitboxes, animations, and attacks to use.

This seemed to be a good code structure, but once the player’s behaviors and actions during the unique attacks began getting integrated, I began to realize that a very large portion of the attacks will require the player to carry out unique actions- for example, floating in the air before slamming into the ground and knocking the surrounding enemies around, or rapidly dashing between any airborne enemies within a certain radius. Should all of these player behaviors be managed in the weapons’ code? Or should these functions be kept in the player code? What would be the best practice for managing these behaviors that affect the player, as well as the enemies? Should the weapons have functions to modify the enemy/player’s states, movements, and behaviors, or should those remain in the respective entity’s code? What would be the best structure for this sort of design? I’m not sure if this is enough information, or if it’s helpful enough of a description, but that’s the gist of the current design of the system. Would love any advice or thoughts for best practices!

1 Like

Player behavior should be managed in player code.

1 Like

Hello! That’s pretty good question.

I geniunely don’t know the actual way you’ve structured your weapon system…

I have two answers: short one and long one. The long one explains more what I mean and perhaps may give you better ideas on organization to your game.

Short answer:

If your weapon, for example, makes player move backwards (buckshot system) then your weapon should call the related util function from the weapon script in player script. Let’s say player script have function force_move_direction(direction : Vector3, force : float). In your weapon script you would just do this player.force_move_direction(Vector3.BACK, 5.5)

Long answer:

But let’s say… uh well I will give you example of implementation and you gonna understand what I mean because your problem is actually about the game organization:

Let’s say there is Node which is a scene that has Script attached to it with the name weapon_manager.gd and this script manages animations, sounds and loads weapons.
There is second Script Resource which is not attached to anything (it’s a script that extends Resource and has class_name Weapon at the very top of the script). The Script resource just have functions like reload(), shoot() and etc., in this script the viewmodel (or whatever you use) animations are played using functions from weapon_manager.gd.

The same Resource Script that has class_name Weapon have multiple @export variable to setup specific things for specific weapons. Example: @export var shoot_anim : String #shoot anim name

Now why I would mention all of that? To setup context and perhaps give you more better organization approach.

Due to the fact that we have Resource which is Weapon… We can create in the File System window a weapon! How? Just Right Mouse Button to add Resource. In that list find your Resource with the name of Weapon. Let’s name it “glock17”.
Now we have created a Weapon resource with the name glock17.tres.

We have @export variables in that Resource! How convenient, now you are able to specify specific settings and parameters for specific weapons! Different specified animation for glock17.tres and different specified animation for m4a1.tres.


I hope you see what I mean. You can create Resource script type with the class_name Resource. In that Script you can have all weapon specific settings. In this script you can setup specific @export var some_feature_enabled : bool = false; settings for specific features.

If you have too specific weapon that requires more @export variables and you want more organized approach you can create can create Script which has extends Weapon and with class_name WeaponMelee.


Now you’ve probably thinking while reading this… when I will answer your actual question on managing the player specific things during the fire shooting actions on the weapons?

Alright, let’s say the same new Script we’ve created that is WeaponMelee there is overriden shoot() function (well we can’t rename it, because we need to override shoot() from the Weapon script we extend from). This function will lead to… weapon_manager.gd.

This means in the original Weapon resource Script there is var weapon_manager : WeaponManager (the weapon_manager.gd should have class_name WeaponManager).

You just call in the shoot() function specific action you need like this weapon_manager.move_player_to_the_back(5.5).

Your weapon_manager will have the @export var player : CharacterBody3D variable. Now the function that we’ve mentioned above in weapon_manager will look like this:

@export var player : CharacterBody3D
# yeah this above means that the `weapon_manager` node is part of the main game scene where the player is. If you have different player spawn system you can remove @export and use some kind of function to give the value to the `player` variable.

func move_player_to_the_back(force : float):
	player.force_move_in_direction(Vector3.BACK) #implies that you're making 3D game but it doesn't matter you can change for your own needs it's just example.

NOW in the player movement script you would have smth like this:

# some player stuff y'know _physics_process and etc.

func force_move_in_direction(direction : Vector3):
	#self.velocity = smth smth smth smth direction

I hope you understand what I mean… just have the needed utils functions in your player script. You call these functions for specific needs in specific weapons that have their own shoot() functions overriden by using the Resource system in Godot Engine.

For the enemies it’s WAY simpler.

As from my organization above we use specific each new Resource for each specific new type of weapon. Example: WeaponMelee.

I don’t know how you did the shoot() part in your project. Like do you use raycasting to get what your weapon shot to…

In any way you probably receiving the object of an enemy after shooting at him in your script. (According to my organization this should be in shoot() function…)

You can perform simple check:

# Let's say it's just normal `Weapon`. But it's kind of a shotgun y'know.

@export var damage : int = 10;

func shoot():
	# some operations you perform to get the enemy object node...

	# your enemy should be organized like a player...
	# he should have a script in his root node! (script attached to CharacterBody)
	# we're checking if his script has method "take_damage".
	if obj.has_method("take_damage"):
		obj.take_damage(self.damage);

Now your enemy script would look like this:

# smth smth smth

func take_damage(damage : float):
	# subtract health like so health - damage

Now because according to my organization we have… each new weapon resource that is basically specific script with all specific settings for each new specific weapon type… the shoot() may have more functionality!

What I mean by that? Well… in my previous example we we’re just checking for take_damage. Now imagine if we want an enemy to get pushed from the player (because we shoot at him). We could check for the another function if obj.has_method(“push_back”) and call that function.

This would imply that all enemies need to have this method, basically all enemies would have some kind of “Base script”.


Due to this implication I suggest you a solution to this organization problem.

Create a script like so:

class_name Enemy
extends CharacterBody3D # or 2D depends on your game type y'know.

# all basic functions like _ready and _physics_process and etc.

func take_damage(damage : float):
	pass;

func push_direction(direction : Vector3, force : float):
	pass;

# and etc. other functions

Now let’s say you have SPECIAL enemy type. Just create a script that goes like this extends Enemy. Now you’re good! You don’t need to write again all those basic functions because they automaticaly extend from the class Enemy!

Now for that special enemy type you can do all special things you need.

You can also always override some functions if needed e.g.

extends Enemy

# all your stuff like _ready, _physics_process and etc.

func take_damage(damage : float):
	# let's say this is special enemy that takes damage in more special way
	# like imagine it has an armor or it actually drains some part of the damage so it receives only small portion of the actual damage given.

If you have any more questions to specify things you can always reach out to me.

1 Like

How and when would you assign a node reference to a variable stored in a resource object?

1 Like

Well, let’s say in the weapon_manager.gd there is _ready function. Which points to the function _update_weapon_model.

Now what is the point of this is that the function _update_weapon_model basically checks what current weapon the player have. If the current weapon is not null then setup everything needed for that weapon (shaders, parameters and etc.). While also pointing in the WeaponResource which the player equipped that current_weapon.weapon_manager = self.

So basically when we need to update the weapon when player switched it we perform all things needed for that and then set on that weapon resource object what is the value of weapon manager.

I hope I explained it clearly.

Thank you so much! I’d been a bit busy thanks to work, but I’ve finally been able to check out this solution! Yeah, this sort of structuring makes a lot more sense! I realized after reading this that there were a lot of portions with code I could have reused, especially using the utils functions in the player or enemy script itself. It feels a bit more intuitive this way, having enemy behaviors stay in the enemy code, the player’s behaviors happening in the the player’s code, all while managing these util calls when needed in the weapons’ code. I was being way too caught up in trying to keep everything in one place, such as altering player/enemy positions in the weapons’ code and conflicting with their respective movement code (e.g. freezing them up in the air, which began having unintended behaviors afterwards like warped gravity or stuttered movement), when it makes a lot more sense to have each node manage its own behavior, and deal with any changes during its own behavior processes! Thanks again for the detailed answers!

1 Like

You’re always welcome! We all learn something new. Have a good day and goodluck in the development of your game!

1 Like

Injecting the manager dependency here kinda breaks the encapsulation of weapon objects. It also creates a circular dependency as the injection is performed by the manager itself.

On top of that it breaks the hierarchy of responsibility, as now weapons are telling their manager what to do, instead of manager doing its job of managing them. Subordinates never give orders to managers. Otherwise they undermine manager’s authority and bring into question the whole purpose of manager’s existence.

Wouldn’t it be better for a weapon to just emit signals for various player reactions? That way it will not depend on knowing the manager’s interface, making the weapon object behave like an independent module.

1 Like

In my case weapon weapon_resource.gd have functions only regarding shooting, reloading, equipping and unequipping. It is needed just for the sake of OOP. When like there is just let’s say you need melee weapon and you just extends WeaponResource on the melee_weapon.gd script.

The weapon_manager.gd just has functions like play_sound, play_anim, update_weapon_model and functionality in general to control the weapons like switch them, equip, unequip, shoot and reload.

The variable weapon_manager in the weapon_resource.gd is required only to call the functions like play_sound and play_anim. Only for this and nothing else. It’s just more compact instead of using additional script like weapon_utils.gd. The OP may go additional route though, he may actually want to use additional script, but it’s just the way I prefer it because both weapon_utils.gd and weapon_manager.gd would basically require same things.


Your point is really right and I can’t say anything against it because it’s right.

But it’s not the weapon resources giving orders to the weapon manager. Weapon manager is still the one who calls on the weapon resource to equip or unequip depending whether player switched the weapon or not. Weapon manager is still the one who calls to shoot and reload on the weapon resource if player clicked the specified keys.

In my architecture scenario the weapon manager is more like of a transition layer between the player and the weapon resources. (Did I mention previously that the weapon manager has _unhandled_input(event) in use to do whatever it needs to with the current_weapon).

As I said in my case scenario it’s just… well it’s just a scene with a Script attached to it that is weapon_manager.gd which is placed under the player scene (if for his functionality it is required to have player object) or the main game scene.

How it is not? It’s telling the manager to do something with the player, resulting in the weapon managing the weapon manager and in turn the weapon manager managing the player. Quite a mess in respect to nominal responsibilities.

Well, yeah you’re right on that.

It’s just more compact way to have util functions to do smth with the player on the same script without using additional script like weapon_utils.gd.

Well, if we just add additional script weapon_utils.gd and instantiate it in the each equipped WeaponResource then the weapon resources would not call the manager to do smth with the player…


Y’know, perhaps it is a great idea to have signals on the weapon resource to do smth with the player. But to be honest I am geniunely not sure, isn’t the result will be the same?

Well the whole point is to have the same result but simpler and more coherent architecture.

1 Like

Yes, you’re right.

I’ve just searched it up and I geniunely forgot about the signals… wow.


It is right to have custom signals on the WeaponResource like this signal play_anim(name : String) and then on the same WeaponResource just play_anim.emit("shoot") when needed e.g. in shoot() function.

The weapon_manager.gd on the other hand would be quite interesting on that part. There is _update_weapon_model function which basically setups everything needed for the weapon to function.
Now technically the weapon_manager.gd would have stored the previously equipped weapon if the player equips a new weapon and then the manager would disconnect the custom signal (e.g. current_weapon.play_anim.disconnect()).
In any way the weapon_manaer.gd would get the new weapon and set it on the current_weapon variable and then just do current_weapon.play_anim.connect(_play_anim).

So it solves the problem that each WeaponResource is it’s own independent resource that has it’s own variables and signals. So we’re just disconnecting from the previous signals and connecting to the new ones when equipped a new weapon or just equipped a weapon in general.

Sorry, that would be more right in terms of the ordering between the manager and it’s resources.


Please, @lunashiba I would like to ask you also look into this.
Even though just having functions like play_anim in the WeaponManager and calling it from the WeaponResource will give you good result it’s not technically correct to do so in terms of the architecture.

It would be actually better to setup signals on the WeaponResource so the WeaponManager would NOT receive direct orders from the WeaponResource because it’s a manager.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.