Optimizing attacks & code efficiency

I have 15 or so items currently in the game, but plan to have hundreds. Each item is a scriptable object. Each weapon item is mapped to a particular attack in my Attacks.cs class based on its name and a bunch of IF statements, and each attack follows the same basic pattern. That is, spawn attack (missile, ball etc), place on cooldown, reset cooldownBoolean after attack speed so that attacks can’t be spammed. This can be seen below:

using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine.EventSystems;
using UnityEngine;

public class Attacks : MonoBehaviour
{

    public EventSystem Event;
    public GameObject spawnEgg;
    private int maxSpawnEggs = 3;
    public List<GameObject> spawnEggs = new List<GameObject>();

    private float spawnEggAS = 0.1f;
    private bool spawnEggCD = true;
    public GameObject fireExplosion;
    private float explosionAS = 0.5f;
    private bool explosionOffCD = true;
    public GameObject player;
    public float explosionRange;
    public AudioManager AudioManager;
    public GameObject MagicBall;
    public GameObject Karma;
  
    // bool for karma cooldown
    private bool karmaOffCD = true;

    // Karma attack speed
    private float karmaAS = 3f;
    public GameObject bomb;
    private bool bombOffCD = true;
    private float bombAS = 0.5f;
    public GameObject missileSeeker;
    private bool missileSeekerOffCD = true;
    private float missileSeekerAS = 0.5f;
    public GameObject missileHoming;
    private bool missileHomingOffCD = true;
    private float missileHomingAS = 0.5f;
    public GameObject LightningBall;
    private bool lightningBallOffCD = true;
    private float lightningBallAS = 2f;
    private Vector3[] spawnLocationsMagicBall;
    public int magicBallSpawnAmount;
    public GameObject ElectricBall;
    private bool ElectricBallOffCD = true;
    private float ElectricBallAS = 0.5f;

    // Delay between each bullet of a burst is fired
    private float ElectricBallDelay = 0.05f;
    private int ElectricBallSpawnAmount = 5;
    public float electricBallForce;
    public float blockSpawnDistace;
    public Equipment selected;
    public Equipment selectedOff;
    public GameObject Wand_Stream_Pellet;
    private float pelletForce = 100f;
    public delegate void AttackMethod();
    public AttackMethod attackFunction;
    public AttackMethod attackFunctionOff;
    public GameObject Zombie;
    private int maxZombies = 3;
    public List <GameObject> currentZombies = new List <GameObject>();
    private bool zombieSpawnOffCD = true;
    private float zombieAS = 0.3f;
    private float zombieLifetime = 20f;
    [SerializeField] GameObject clusterBomb;
    bool clusterBombOffCD = true;
    float clusterBombAS = 0.5f;
    [SerializeField] GameObject sniperBlast;
    bool sniperBlastOffCD = true;
    float sniperBlastAS = 2f;
    [SerializeField] float sniperBlastForce = 100f;
    [SerializeField] GameObject arcaneBlast;
    public bool arcaneBlastOffCD = true;
    float arcaneBlastAS = 4f;
    void Start()
    {
        // Locations start at top of character going clockwise
        spawnLocationsMagicBall = new Vector3[8];
        spawnLocationsMagicBall[0] = new Vector3(0f, 1f, 0);
        spawnLocationsMagicBall[1] = new Vector3(0.5f, 0.5f, 0);
        spawnLocationsMagicBall[2] = new Vector3(1f, 0f, 0);
        spawnLocationsMagicBall[3] = new Vector3(0.5f, -0.5f, 0);
        spawnLocationsMagicBall[4] = new Vector3(0f, -1f, 0);
        spawnLocationsMagicBall[5] = new Vector3(-0.5f, -0.5f, 0);
        spawnLocationsMagicBall[6] = new Vector3(-1f, 0f, 0);
        spawnLocationsMagicBall[7] = new Vector3(-0.5f, 0.5f, 0);

        magicBallSpawnAmount = 8;
        electricBallForce = 70f;

        player = gameObject;
        explosionRange = 5f;
        AudioManager = AudioManager.instance;

        karmaAS = 1f;

        attackFunction = attackSelection(selected);
        attackFunctionOff = attackSelection(selectedOff);
      
        Event = GameObject.Find("EventSystem").GetComponent<EventSystem>();
    }

    public void updateSelection(){
        selected = GetComponent<PlayerHotBar>().selected;
        selectedOff = GetComponent<PlayerHotBar>().selectedOff;

        attackFunction = attackSelection(selected);
        attackFunctionOff = attackSelection(selectedOff);
    }

    void Update(){

        try
        {
            if (Input.GetButton("Main") && !EventSystem.current.IsPointerOverGameObject()){
            attackFunction();
            }
            if (Input.GetButton("Alt") && !EventSystem.current.IsPointerOverGameObject()){
                attackFunctionOff();
            }
        }
        catch
        {
          
            Debug.Log("No attack selected");
        }

        if (Input.GetKeyDown(KeyCode.Q)){
            spawnMagicBall();
        }

        if(currentZombies.Count > 0){
            currentZombies.RemoveAll(GameObject => GameObject == null);
        }
    }

    AttackMethod attackSelection(Equipment Equipped){

        if(Equipped != null){

            if(Equipped.isMelee){
                return Melee;
            }

                if(Equipped.name == "Wand_Stream"){
                    return Electric_Ball;        
                }

                else if(Equipped.name ==  "Launcher"){
                    return missile_Homing;
                }

                else if(Equipped.name == "Staff_Lightning"){
                    return Lightning_Ball;
                }

                else if(Equipped.name == "Staff_Explosion"){
                    return Explosion;
                }

                else if (Equipped.name == "Staff_Holy"){
                    return karma;
                }

                else if (Equipped.name == "Staff_Vine"){
                    return ArcaneBlast;
                }

                else if (Equipped.name == "Staff_Undead"){
                    return ZombieAlly;
                }
                else if(Equipped.name == "Launcher_Seeker"){
                    return missile_Seeker;
                }
                else if(Equipped.name == "SpawnEgg"){
                    return throwEgg;
                }
                else if(Equipped.name == "Staff_Sword"){
                    return clusterBombFire;
                }
        }
        return doNothing;
    }

    void doNothing(){
    }

    void Melee(){

    }

    void ArcaneBlast(){
        if(arcaneBlastOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(arcaneBlast,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * 300f);

            arcaneBlastOffCD = false;
            WaitAndDo(arcaneBlastAS, () => arcaneBlastOffCD = true);
        }
    }

    void SniperBlast(){
        if(sniperBlastOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(sniperBlast,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            // Points missile at mouse pos

            float rot_z = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            objectInstance.transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);

            objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * sniperBlastForce);

            sniperBlastOffCD = false;
            WaitAndDo(sniperBlastAS, () => sniperBlastOffCD = true);
        }
    }

    void throwEgg(){
        if(spawnEggs.Count < maxSpawnEggs && spawnEggCD == true){

            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(spawnEgg,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            // Points object at mouse pos

            float rot_z = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            objectInstance.transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);

            objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * 500f);
            spawnEggs.Add(objectInstance);

            spawnEggCD = false;
            WaitAndDo(spawnEggAS, () => spawnEggCD = true);
        }
    }
    void clusterBombFire(){
        if(clusterBombOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(clusterBomb,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            // Points missile at mouse pos

            float rot_z = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            objectInstance.transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);

            objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * 500f);

            clusterBombOffCD = false;
            WaitAndDo(clusterBombAS, () => clusterBombOffCD = true);
        }
    }

    void bombLaunch(){
        if(bombOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(bomb,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            // Points missile at mouse pos

            float rot_z = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            objectInstance.transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);

            objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * 500f);

            bombOffCD = false;
            WaitAndDo(bombAS, () => bombOffCD = true);
        }
    }

    void missile_Seeker(){
        if(missileSeekerOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(missileSeeker,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            // Points missile at mouse pos

            float rot_z = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            objectInstance.transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);

            missileSeekerOffCD = false;
            WaitAndDo(missileSeekerAS, () => missileSeekerOffCD = true);
        }
    }

    void missile_Homing(){
        if(missileHomingOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(missileHoming,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            // Points missile at mouse pos

            float rot_z = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            objectInstance.transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);

            missileHomingOffCD = false;
            WaitAndDo(missileHomingAS, () => missileHomingOffCD = true);
        }
    }

    void Lightning_Ball(){

        if(lightningBallOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(LightningBall,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * electricBallForce/3);

            lightningBallOffCD = false;
            WaitAndDo(lightningBallAS, () => lightningBallOffCD = true);
        }
    }

    void ZombieAlly(){

        if(zombieSpawnOffCD == true && currentZombies.Count < maxZombies){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            Vector3 spawnPos = UnityEngine.Camera.main.ScreenToWorldPoint(Input.mousePosition);
            spawnPos.z = 0;

            GameObject objectInstance = Instantiate(Zombie,
            spawnPos, Quaternion.Euler(new Vector3(0, 0, 0)));

            objectInstance.GetComponent<Enemy>().setAllegiance("Ally");
            objectInstance.GetComponent<Enemy>().Invoke("allyDeath", zombieLifetime);
            currentZombies.Add(objectInstance);
            zombieSpawnOffCD = false;
            WaitAndDo(zombieAS, () => zombieSpawnOffCD = true);
        }

    }

    void Wand_Stream(){

        Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
        Vector3 dir = (Input.mousePosition - sp).normalized;

        var randX = UnityEngine.Random.Range(-.5f, .5f);
        var randY = UnityEngine.Random.Range(-.5f, .5f);

        Vector3 Pos = new Vector3(transform.position.x + randX, transform.position.y + randY, 0);

        GameObject objectInstance = Instantiate(Wand_Stream_Pellet,
        Pos, Quaternion.identity);

        objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * pelletForce);
    }
    void karma(){

        if(karmaOffCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(Karma,
            gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * electricBallForce);
            karmaOffCD = false;
            // Invoke("karmaCDReset", karmaAS);
            WaitAndDo(karmaAS, () => karmaOffCD = true);
        }
    }

    void Electric_Ball(){

        if(ElectricBallOffCD == true){
          

            for (int i = 0; i < ElectricBallSpawnAmount; i++)
            {
                Invoke("spawnElectricBall", ElectricBallDelay*i);
            }
            ElectricBallOffCD = false;
            WaitAndDo(ElectricBallAS, () => ElectricBallOffCD = true);
        }
    }

    // Call this function as --> WaitAndDo(karmaAS, () => karmaOffCD = true);
    // Put in bool and float attack speed and works!

    public Coroutine WaitAndDo(float timeInSeconds, Action action)
    {
        return StartCoroutine(Execute(timeInSeconds, action));
    }
    private IEnumerator Execute(float timeInSeconds, Action action)
    {
        yield return new WaitForSeconds(timeInSeconds);
        if (Application.isPlaying) action();
    }

    void spawnElectricBall(){

        Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
        Vector3 dir = (Input.mousePosition - sp).normalized;

        GameObject objectInstance = Instantiate(ElectricBall,
        gameObject.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));
        objectInstance.GetComponent<Rigidbody2D>().AddForce(dir * electricBallForce);
    }
    void spawnMagicBall(){


        GameObject [] Attacks = GameObject.FindGameObjectsWithTag("Attack");
        GameObject [] magicBalls = new GameObject [Attacks.Length];
        for (int i = 0; i < Attacks.Length; i++)
        {
            // if (Attacks[i].GetComponent<AttackClass>().Type == "MagicBall"){
            //     magicBalls[i] = Attacks[i];
            // }
        }

        if (magicBalls != null){
            for (int i = 0; i < magicBalls.Length; i++) {
            Destroy(magicBalls[i]);
            }
        }

        int v = 0;

        for (int i = 0; i < magicBallSpawnAmount; i++) {
            GameObject objectInstance = Instantiate(MagicBall,
            spawnLocationsMagicBall[v] + player.transform.position
            , Quaternion.Euler(new Vector3(0, 0, 0)));

            if (magicBallSpawnAmount == 4){
                v = v+1;
            }

            else if (magicBallSpawnAmount == 2){
                v = v+3;
            }
            v++;
        }
  
    }

    void Explosion(){

        if(explosionOffCD == true){
            Vector3 playerPosition = player.transform.position;

            Vector3 spawnPosition = UnityEngine.Camera.main.ScreenToWorldPoint(Input.mousePosition);
            spawnPosition.z = 0.0f;

            // If its in explosionRange spawn where it is
            if (Vector3.Distance(playerPosition, spawnPosition) <=explosionRange){
                AudioManager.GetComponent<AudioManager>().PlaySound("Explosion");
                GameObject objectInstance = Instantiate(fireExplosion,
                spawnPosition, Quaternion.Euler(new Vector3(0, 0, 0)));
                explosionOffCD = false;
                WaitAndDo(explosionAS, () => explosionOffCD = true);
            }

            // Else do calculations

            else {
                Vector3 newSpawnPosition = spawnPosition;

                bool toRight = true;
                bool abovePlayer = true;

                // If spawn position is to the right of player

                if (spawnPosition.x > playerPosition.x){
                    toRight = true;
                }

                else {
                    toRight = false;
                }
              
                // If spawn position is above player

                if (spawnPosition.y > playerPosition.y){
                    abovePlayer = true;
                }

                else {
                    abovePlayer = false;
                }

                // If its to the right and out of explosionRange

                if (toRight == true){
                    if ((spawnPosition.x - playerPosition.x) > explosionRange){
                    newSpawnPosition.x = playerPosition.x + explosionRange;
                    }
                }

                else {
                    if ((playerPosition.x - spawnPosition.x) > explosionRange){
                        newSpawnPosition.x = playerPosition.x - explosionRange;
                    }
                  
                }              

                if (abovePlayer == true){
                    if ((spawnPosition.y - playerPosition.y) > explosionRange){
                    newSpawnPosition.y = playerPosition.y + explosionRange;
                    }
                }

                else {
                    if ((playerPosition.y - spawnPosition.y) > explosionRange){
                        newSpawnPosition.y = playerPosition.y - explosionRange;
                    }
                }
                AudioManager.GetComponent<AudioManager>().PlaySound("Explosion");
                GameObject objectInstance = Instantiate(fireExplosion,
                (newSpawnPosition), Quaternion.Euler(new Vector3(0, 0, 0)));
                explosionOffCD = false;
                WaitAndDo(explosionAS, () => explosionOffCD = true);
            }
        }
          
          
    }
    public GameObject FindClosestObject(string name, Vector3 Position)
    {
        GameObject[] gos;
        gos = GameObject.FindGameObjectsWithTag(name);
        GameObject closest = null;
        float distance = Mathf.Infinity;
        Vector3 position = Position;
        foreach (GameObject go in gos)
        {
            Vector3 diff = go.transform.position - position;
            float curDistance = diff.sqrMagnitude;
            if (curDistance < distance)
            {
                closest = go;
                distance = curDistance;
            }
        }
        return closest;
    }
}

This is obviously bad practice as there will be way too many variables and functions to count. So what i want to be able to do is optimize this code and make it lots more efficient, so that i don’t have to keep writing out variables, functions and if statements for each weapon i create.

Here is my scriptableobject class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine.Tilemaps;
using enums;
using UnityEngine;

[CreateAssetMenu(fileName = "New Equipment", menuName = "Inventory/Equipment")]
public class Equipment : InventoryItem
{
    public EquipmentSlot equipSlot;
    public GameObject prefabItem;
    public Tile prefab;
    public bool hasGravity;
    public string type;
    public int armorModifier;
    public int damageModifier;
    public float speedModifier;
    public float jumpForceModifier;
    public float miningEfficiency;
    public bool providesLight = false;
    public bool rockFriend = false;
    public bool lightningProtection = false;
    public int soulCost = 10;
    public bool isMelee = false;
    public float meleeBaseDamage = 0;
    public float meleeHitRadius;
  
    public override void Use(){
        base.Use();
        Player = GameObject.Find("Player");
        RemoveFromInventory();
        Player.GetComponent<EquipmentManager>().Equip(this);
    }
}

namespace enums{
    public enum EquipmentSlot {MainHand, Head, Chest, Legs, Gloves, Feet, Necklace, Ring}
}

// public enum EquipmentSlot {MainHand, Head, Chest, Legs, Gloves, Feet, Necklace, Ring}

public enum Type {Block, Weapon, Armor}

One solution i thought of was to create a ‘Weapon’ class that inherits from ‘Equipment’ that i can create a scriptable object from, and my Attacks.cs class would use variables from that scriptableobject instead.

Example:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "New Equipment", menuName = "Inventory/Weapon")]
public class Weapon : Equipment
{
    public bool offCD = true;
    public float AS = 1f;
    public GameObject Attack;
    public float force;

}

Would this work? Is there a better solution i could implement? Any help would be appreciated, thanks.

P.S not all attacks will work the same. Most will follow the basic formula of spawn attack and reset CD etc but some will have different functions, like spawning 5 missiles instead of 1 etc.

Hi,
For the particular problem you can create inheritance hierarchy with ex. item parent → weapon child. That is the solution for your repeating variables that you create for every weapon. You could also define an abstract Attack() method that each child would have to implement.

public abstract class Item : ScriptableObject{
//variables for every item
}
public Weapon : Item{
//weapon related attributes
public abstract void Attack();
}

It doesn’t matter what you do in the attack method. You would simply create create a separate RocketWeapon child in your inheritance that defines Attack as a virtual method with the functionality you desire. (Also do mind that in each attack method you currently have you duplicate a lot of code and inheritance hierarchy will fix that)

public RocketWeapon : Weapon
{
public void virtual Attack(){
//shoot rockets
}
}

Now having all of that you should really use FactoryMethod instead of those ugly if else statements or just have 1 Weapon object and call on it Weapon.Attack() and swap it for a different object to make use of the attack.

Let me know if you have any more questions!

Switching to ScriptableObjects would be a huge improvement over the old system, so I think you’re on the right path.

Since the plan is to have hundreds of items, you might want to consider using composition over inheritance as much as possible in your items, otherwise you might easily end up rewriting the same code a lot.

You could use a component system for your items, where every time an item is used any number of effects components could be applied. So something similar to Unity’s GameObject-Component system, but for your ScriptableObject items.

Something to this effect:

public abstract class Equipment : ScriptableObject, IItem
{
    protected StatModifier[] whileWornEffects; // armor bonus, stat bonuses, curses etc.
    protected Effect[] onEquippedEffects        // on equip sound effect etc.
    protected Effect[] onUnequippedEffects;
    protected HitEffect[] onHitTakenEffects; // on hit visual effect etc.

    private void OnEnable()
    {
        Setup();
    }
   
    protected abstract void Setup(); // compose what equipment does here by building all the effects

    public void Equip(Actor subject)
    {
        RemoveFromInventory(subject);
        subject.EquipmentManager.Equip(this);
        OnEquip(subject);
       
        foreach(var effect in whileWornEffects)
        {
            effect.Apply(subject, this);
        }
       
        foreach(var effect in onEquippedEffects)
        {
            effect.Apply(subject, this);
        }
    }

    public void Unequip(Actor subject)
    {
        OnUnequip(subject);
        subject.EquipmentManager.Unequip(this);
        AddToInventory(subject);
       
        foreach(var effect in whileWornEffects)
        {
            effect.Remove(subject, this);
        }
       
        foreach(var effect in onUnequippedEffects)
        {
            effect.Apply(subject, this);
        }
    }

    public void OnHitTaken(Actor subject, Actor attacker, AttackInfo attack)
    {
        foreach(var effect in onHitTakenEffects)
        {
            effect.OnHitTaken(subject, attacker, attack, this);
        }
    }
}

public abstract class Weapon : Equipment
{
    protected Attack[] onAttackEffects; // MeleeAttack, ProjectileAttack etc.
    protected AttackRequirement[] attackRequirements; // CooldownTime etc.

    protected float lastAttackTime;
   
    public float LastAttackTime
    {
        get
        {
            return lastAttackTime;
        }
    }

    protected virtual bool CanAttack(Actor subject)
    {
        foreach(var requirement int attackRequirements)
        {
            if(!requirement.Test(subject, this))
            {
                return false;
            }
        }
        return true;
    }

    public void Attack(Actor subject, Actor target)
    {
       foreach(var effect in onAttackEffects)
       {
              effect.Attack(subject, target, this);
       }
       lastAttackTime = Time.time;
    }
}

Also using variable names like “offCD” and “AS” make the code really difficult to read. Even names that use actual words like “Attack” are still quite ambiguous (“instantiateOnAttacked”?). I would work on making them more clear and unambiguous, so things are still remain manageable once you have hundreds of items.

Yep, in the bottom of my post i showed that i thought of that solution already, thanks.

Sorry i’m i didn’t make it clear, the current system already uses ScriptableObjects. The weapons in my game simply fire objects (prefabs) like missiles, magic missiles etc which already inherit from a class called ‘AttackClass’, and already have hit effects, code for destruction etc. All i do is flip booleans on these attacks to change functionality (like destroying on collision, hitting enemies etc. I don’t see the benefit of using composition here over inheritance? Each attack is different no? So i will have the rewrite how it moves every time anyway?

Your weapons being able to instantiate any prefab is a good example of using composition over inheritance, and that would already help a lot in avoiding repeating yourself in code.

Instantiating a projectile might not make sense with things like melee weapons, potions and such though, so you might want to consider support alternative composition patterns for items like those.

It seems to me like the approach you currently have with your items is still to include a lot of code in your base classes, and then extending classes are mostly just flipping boolean values to determine their behaviour.

This means that all of your equipment would have lots of data like “hasGravity”, “providesLight”, “rockFriend” and “meleeHitRadius”, even if they bare no relation to the actual behaviour of the item. Once you have hundreds of items, your base classes would probably contain thousands of lines of code and become very difficult to read. With dozens of if-statements in the code cyclomatic complexity would get really high, making debugging very hard.

Extracting these behaviours into smaller modules that you can then only add to items that actually need them would go a long way in resolving these issues.

1 Like

Hmm, i guess i just don’t fully grasp composition yet.

Yep, my next step is to separate Equipment into different classes. Currently its a mess, but i have never used ScriptableObects, inheritance (in unity) or an item system before this project so that’s my excuse. Cheers.

1 Like

Sorry I misunderstood your question. If you have many different attacks that you want to perform and they share the same features than try separating the differences and adding them through a builder pattern.
If you want to use your scriptable objects inheritance system then take a look at factory pattern.
From what I’m seeing you are looking at which is better inheritance or composition but those are the tools to create the solution. Design patterns gives you a generic solution and I believe that is what you want to look for.
Basic difference between composition and inheritance usually boils down to “do I have to shared data / methods” if yes - > use inheritance if you just need different functionality use composition.
Take a look at this if you are not sure

https://www.youtube.com/watch?v=8TIkManpEu4

1 Like

Hmm ok thanks. So i have my AttackFunction working properly:

public class Weapon : Equipment
{
    public bool offCD = true;
    public float AS = 1f;
    public GameObject Attack;
    public float force;

    public void AttackFunction(){

        Player = GameObject.Find("Player");


        if(offCD == true){
            Vector3 sp = Camera.main.WorldToScreenPoint(Player.transform.position);
            Vector3 dir = (Input.mousePosition - sp).normalized;

            GameObject objectInstance = Instantiate(Attack,
            Player.transform.position, Quaternion.Euler(new Vector3(0, 0, 0)));

            // Points missile at mouse pos

            float rot_z = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            objectInstance.transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);

            offCD = false;
            Player.GetComponent<AttackCooldowns>().WaitAndDo(AS, () => offCD = true);
        }      
    }

}

This is the attack function that works for most of my attacks. However some have different ways of spawning. Some attacks i don’t want any of that code, some of them i only want a small part. So maybe half my attacks can function with the standard ‘AttackFunction’. How and where do i write code for the other attacks that behave differently?

EDIT: Do i have to write a new script that inherits from ‘Weapon’ and overrides ‘AttackFunction’?

Yes you could just inherit from weapon and override AttackFunction. That is one route.

Alternatively you could extract that AttackFunction into its own class, like MeleeAttack, and then use that class only inside the weapons that need that functionality.

The latter option would help keep your classes cleaner, and would allow you to easily combine the functionality of multiple attack components, compared to just using simple inheritance.

This video contains brief explanations of both approaches:

1 Like

Hi, watched both the videos but am still a little confused.

So say i have four ranged attacks, each assigned to a different weapon (item). The first three use the standard attackfunction, which is just launching the projectile at the direction of the mouse position from the Player position. However for my fourth attack, i want the attack to launch three projectiles with a wait time between each firing. Now i already have this implemented into my game, but how would i implement this into the new system? So i have this code for the fourth attack:

void StartAttack(){

        for (int i = 0; i < shotsPerAttack; i++)
        {
            Invoke("Shoot", waitTimeBetweenShots * i);
        }
    }

    void Shoot(){

        var projectileInstance = Instantiate(projectile, transform.position, Quaternion.identity);

        Vector2 shotPos = (Vector2)target.transform.position + (Random.insideUnitCircle * shotRadius);

        var direction = shotPos - (Vector2)transform.position;

        projectileInstance.GetComponent<Rigidbody2D>().AddForce(direction * projectileForce, ForceMode2D.Impulse);

    }

Where does this code go? How can i implement the fourth attack? My understanding from your comment is i make an AttackFunction class, but i’m not sure how i implement this or how i assign different attack functions to different weapons.

Thanks.

The idea is that you create many different attacks.

public abstract class Attack : ScriptableObject
{
    public abstract bool CanAttack(Actor attacker);
    public abstract void OnAttack(Actor attacker);
}

[CreateAssetMenu(fileName = "New Projectile Attack", menuName = "Inventory/Attack/Projectile")]
public class ProjectileAttack : Attack
{
    public GameObject projectilePrefab;   
    public bool offCD = true;
    public float AS = 1f;
    public float force;
    public override bool CanAttack(Actor attacker)
    {
        return offCD;
    }
    public override void OnAttack(Actor attacker)
    {
        var transform = attacker.transform;
        Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
        Vector3 dir = (Input.mousePosition - sp).normalized;
        float rotationZ = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
        var rotation = Quaternion.Euler(0f, 0f, rotationZ - 90f);
        var projectileInstance = Instantiate(projectile, transform.position, rotation);
        offCD = false;
        attacker.GetComponent<AttackCooldowns>().WaitAndDo(AS, () => offCD = true);
    }
}

[CreateAssetMenu(fileName = "New Triple Projectile Attack", menuName = "Inventory/Attack/Triple Projectile")]
public class TripleProjectileAttack : ProjectileAttack
{
    public int shotsPerAttack;
    public float waitTimeBetweenShots;
   
    protected IEnumerator firingThreeShots;
    public override void OnAttack(Actor attacker)
    {
        firingThreeShots = FireThreeShots(attacker);
        attacker.StartCoroutine(firingThreeShots);
    }
   
    protected IEnumerator FireThreeShots(Actor attacker)
    {
        for(int i = 0; i < shotsPerAttack; i++)
        {
            base.OnAttack(attacker);
            yield return new WaitForSeconds(waitTimeBetweenShots);
        }
    }
}

Then you only need one Weapon class, and you can create new weapons just by specifying which attack(s) they should use.

[CreateAssetMenu(fileName = "New Weapon", menuName = "Inventory/Weapon")]
public class Weapon : Equipment
{
    public Attack attack;
   
    public bool CanAttack(Actor attacker)
    {
        return attack.CanAttack(attacker);
    }
   
    public void Attack(Actor attacker)
    {
        attack.OnAttack(attacker);
    }
}
1 Like

Oh i see, so you create attacks through Scriptable objects as well. Thanks.

What is the Actor type in this scenario btw? What do i replace it with?