Hello,
I’m currently working on a project where my character can hold multiple weapons simultaneously (around six) and shoot them automatically at enemies.
Here’s how I’ve structured the system:
- Weapon Data (Scriptable Object): Stores all the initial data for each weapon.
- Weapon Class: Contains the same attributes as the Weapon Data Scriptable Object, which are assigned through a constructor whenever the player acquires a new weapon.
- Player Weapon Controller (MonoBehaviour): Manages an inventory that holds all the current weapons (instances of the Weapon Class). Weapons are instantiated using the Weapon Class when needed.
I’m struggling to figure out where to place the logic for shooting all these weapons, especially since they need to fire simultaneously, each with its own properties like range, enemy detection, type, and so on.
Initially, I thought about handling everything within the PlayerWeaponController
, but the unique behaviors and requirements of each weapon make this approach overly complex.
Could someone with more experience give me some pointers on how to approach this?
Thank you!
I’d probably just use a Behavior graph per weapon. So I might have a wand plays sound effects / particles from the wand and spawn a lighting bolt effect at the enemy. Or maybe it has more complex logic like a boomerang that spins around and hits multiple targets with a crit chance. I generally find behavior graphs easier for creating complex logic with reusable nodes. You can also reuse logic by nesting graphs. Then the PlayerWeaponController
just needs to tell each weapons behavior graph to activate. Any stats like the amount of damage can just go into the blackboard, which can also be stored in your WeaponData
.
At the core of it you just need a common base class/interface that each weapon expresses, have a collection of base type, and enumerate and activate them.
In essence:
public interface IWeapon
{
void FireWeapon();
}
Of course an actual implementation might be different depending on your requirements.
1 Like
I’ve never heard of the behaviour graph before. Took a look online and it seems interesting indeed. I’ll consider going forward but as of now, I’m pushing myself to better learn to code.
Thank you though for the great tip.
1 Like
Really where you handle that is entirely up to you. My personal preference is to skip the ‘WeaponClass’ object and directly assign the ‘WeaponData’ to the ‘PlayerWeaponController’ and handle the per weapon logic in the ‘WeaponData’ via exposed interfaces, like Spiney suggested.
It looks something like this:
public interface IWeapon
{
StartUse(IWeaponUser user);
ContinueUse(IWeaponUser user);
EndUse(IWeaponUser user);
Canceluse(IWeaponUser user);
}
public abstract AbstractWeaponData : ScriptableObject, IWeapon
{
public virtual void StartUse(IWeaponUser user) {}
public virtual void EndUse(IWeaponUser user) {}
public virtual void CancelUse(IWeaponUser user) {}
}
public WeaponUser : MonoBehaviour, IWeaponUser
{
public AbstractWeaponData CurrentWeapon; //you could also make this IWeapon, but in this case it makes the value assignable in the inspector
public void UseCurrentWeapon()
{
CurrentWeapon.StartUse(this);
//For simple one-shot weapons you might only need 'StartUse'.
//For more complex stuff like weapons that need to charge up,
//or fire continuously then you might to use the other methods somehow here.
}
}
At this point you can subclass each weapon type by deriving from AbstractWeaponData and writing the specific logic for each. The one thing to be aware of in this case is that any data you store on the AbstractWeaponData is obviously shared so if you need per-instance state you’ll need to associate that with the weapon user somehow. That’s why the IWeaponUser is passed as a context in each of the exposed functions.
Also, I have a bit of a nitpick here but why is this just for players? It seems to me that if you go to all of this effort why not use it for anything that can attack?
1 Like
While this does work and I would also suggest to have the logic in the actual weapon type, you will run into instance variable issues this way. So for things like current ammo, cooldown and other state variables you don’t have any place to store them. What state variables you may need would also depend on the weapon type. Storing them in the scriptable object isn’t the best approach when the same type is shared multiple times. So having some kind of state holder class may be necessary.
You could in theory use a generic solution to store arbitrary data by using something like my SimpleJSON. Since it’s essentially a dictionary of specialized types, the access would be quite fast. Also having the data in the json classes makes it easy to save and load it when necessary. I have used my framework in various situations. Since each data types are concrete classes, there is no conversion overhead when used correctly. Only the dictionary lookup may be relevant
Though an intermediate instance class usually don’t hurt. I would probably treat the scriptable objects like “factories” and base value storage and have it create the simple instance classes for you that hold the actual state variables.
2 Likes
Yeah, the per-instance data can be a bit tricky but I tend to think of the functionality of the weapon as being just as readonly as the data itself. In my case that’s why I suggested passing a context to each of the scriptableobject methods so that you can further query for instanced data.
In my own situations I tend to have a setup/breakdown method for the SOs as well and in those methods they register/unregister variables on the WeaponUser’s blackboard to handle such things. It definitely gets a bit trickier there so there are certainly benefits to the intermediate WeaponClass object that is instantiated but in practice it kinda ends up doing exactly the same thing as blackboards and contexts but with a different style. You could probably also make the argument that my method isn’t as performant but unless you’re doing it with thousands of weapons it really won’t matter much I think.
2 Likes