Help with different attack types

Hi,

Im not really that experienced so i would need a help with a part of a project where im stuck :frowning:

Im working on a 2d game where player will have multiple attack types available, for example melee attack1 and attack2, ranged attack1 and attack2. Attacks would have different damage and some other properites, same as animations. Im thinking scriptable objects are way to do different attack types?
Under player object i have childed empty game object that is having box collider that is active only at the time of attack. Ranged attacks are making projectiles that are having their own collider.
My biggest issue here is how to detect which attack type was active during collision with enemy?

Scriptable object:

public class AttackTypes : ScriptableObject
{
    [SerializeField] private int attackDamage;
    [SerializeField] private int manaCostUsage;
    [SerializeField] private bool isAttackAvailable;
}

Attack script thats put on a player object:

public class PlayerAttack : MonoBehaviour
{
    [SerializeField] private Transform projectileSpawnLocation;
    [SerializeField] private GameObject projectilePrefab;
    [SerializeField] private AttackTypes[] attackTypes;

    private Animator animator;
    private BoxCollider2D boxCollider;
    private float facingDirectionAtTimeOfAttack;

    private bool isPlayerUsingRangedAttack = false;

    public float FacingDirectionAtTimeOfAttack
    {
        get
        {
            return facingDirectionAtTimeOfAttack;
        }
    }

    private void Awake()
    {
        animator = GetComponent<Animator>();
        boxCollider = GetComponentInChildren<BoxCollider2D>();
        boxCollider.enabled = false;
    }

void Update()
    {
        if (Input.GetKeyDown(KeyCode.E))
        {
            StartCoroutine(Attack("Attack"));
        }
        else if (Input.GetKeyDown(KeyCode.R))
        {
            isPlayerUsingRangedAttack = true;
            StartCoroutine(Attack("CastAttack"));
        }
    }

private IEnumerator Attack(string attackName)
    {
        animator.SetTrigger(attackName);
        yield return new WaitForSeconds(animator.GetCurrentAnimatorStateInfo(0).length);
        if (isPlayerUsingRangedAttack)
        {
            Vector3 newSpawnPosition = projectileSpawnLocation.position;
            Quaternion newRotationForPosition = projectileSpawnLocation.rotation;
            facingDirectionAtTimeOfAttack = transform.localScale.x;
            Instantiate(projectilePrefab, newSpawnPosition, newRotationForPosition);
            isPlayerUsingRangedAttack = false;
        }
    }

private void AddColliderAnimationEvent()
    {
        boxCollider.enabled = true;
    }

    private void RemoveColliderAnimationEvent()
    {
        boxCollider.enabled = false;
    }

}

Script added to object thats childed to player and having only box collider:

public class PlayerAttackCollision : MonoBehaviour
{
    [SerializeField] private AttackTypes attackType;


    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Enemy")
        {
            //detect which attack type was used and take its stats
        }
    }
}

you can create 2 tags, one for your object with the melee attack collider, and other tag for your projectile prefab. So in your Enemy script you can decide what to do when collide against melee or ranged attack. (So, your “PlayerAttackCollision” script will be not used for decided what to do with the attacks of the player.)

however, if you want to decide between “attack 1” “attack 2” “attack 3” “ranged attack 1” “ranged attack 2” etc. Could be a little more complicated because your code implementation. I think you should revaluate your code and do again.

I would set the projectile prefab GameObject be inactive. Events like Awake and OnCollisionEnter2D are not triggered on the components of inactive GameObject, and so this little trick makes it possible to set the attack type for the projectile after it has been instantiated, but before it can collide with anything etc. Once all data has been setup up properly it can be set active, and off it goes.
Alternatively you could only have the Collider2D component be disabled in the prefab, if the OnCollisionEnter2D is the only event that you care about not firing prematurely.

Then you need to add a method for assigning the attack type for the projectile. Once you have that it’s easy to use it inside the OnCollisionEnter2D function:

public class PlayerAttackCollision : MonoBehaviour
{
    [SerializeField] private AttackTypes attackType;

    public void SetAttackType(AttackTypes setAttackType)
    {
        attackType = setAttackType;
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        var actor = collision.GetComponent<Actor>()
        if (actor != null)
        {
            actor.OnAttacked(attackType);
        }
    }
}

Protip: If you want to take your code to the next level and make it more decoupled, you could also create an interface like IAttackable which your targetable classes implement, instead of directly referring to a class like Actor here.

The Instantiate method gives you a reference to the newly created instance, which we need next so that we can call the SetAttackType method on it.

If you provide the Instantiate method a GameObject prefab it will return a GameObject instance, and then we could call GetComponent(). However we can also avoid that extra step by changing the type of the prefab to be PlayerAttackCollision, in which case Instantiate returns a reference to component of that same type inside the newly created instance.

To do that change the projectilePrefab definition to look like this, and assign the reference again through the inspector:

    [SerializeField] private PlayerAttackCollision projectilePrefab;

Then all that is left is changing the Attack function so that it sets the attack type after instantiating the projectile, and then sets the prefab active. You’ll probably want to add a new parameter for passing the attack type to the function.

    private IEnumerator Attack(string attackName, AttackTypes attackType)
    {
        animator.SetTrigger(attackName);
        yield return new WaitForSeconds(animator.GetCurrentAnimatorStateInfo(0).length);
        if (isPlayerUsingRangedAttack)
        {
            Vector3 newSpawnPosition = projectileSpawnLocation.position;
            Quaternion newRotationForPosition = projectileSpawnLocation.rotation;
            facingDirectionAtTimeOfAttack = transform.localScale.x;
            var projectileInstance = Instantiate(projectilePrefab, newSpawnPosition, newRotationForPosition);
            projectileInstance.SetAttackType(attackType);
            projectileInstance.gameObject.SetActive(true);
            isPlayerUsingRangedAttack = false;
        }
    }

Remember to modify the StartCoroutine calls to include the new parameter too:

StartCoroutine(Attack("Attack", attackTypes[normalAttackTypeIndex]));

I’m not sure how you’re planning to get the right attack type from the attackTypes array here. You might want to consider not using an array at all, but instead having separate fields for each attack type. So simply:

    [SerializeField] private AttackTypes normalAttack;
    [SerializeField] private AttackTypes castAttack;