0
\$\begingroup\$

I have made an up and down movement RPG prototype and I wanted to have a fighting system.

I made it so that the enemy follows the player and that I get damage every time he collides with me. The problem was that he only gave me damage once and would have to stop colliding with me, to attack again. I fixed that by changing OnCollisionEnter2D to OnCollisionStay2D, but now he attacks every frame, which makes him way too strong.

I already asked some people how to change it and they said you could do it with coroutines. The thing is that I have no idea how to code that and it gets even more complicated, because the enemy is constantly touching the player (When you're not dodging). I would be really happy if someone could help me with that

EnemyAttack.cs:

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

public class EnemyAttack : MonoBehaviour
{
    [SerializeField] private GameObject Player;
    [SerializeField] private int timer;
    public int damage;
    public PlayerHealth playerHealth;
    public float AttackCooldown;
    public float WaitForSecondsRealTime;
    float PlayerHealthh;

    private void OnCollisionStay2D(Collision2D collision) {
        if(collision.gameObject.tag == "Player") {
            playerHealth.TakeDamage(damage);

        }
    }
}
\$\endgroup\$

2 Answers 2

0
\$\begingroup\$

I wouldn't use a coroutine for this - just tracking the time since the last attack is enough.

public class EnemyAttack : MonoBehaviour
{
    public int damage;
    public PlayerHealth playerHealth;
    public float attackCooldown;

    float _lastAttackTime;

    private void OnCollisionStay2D(Collision2D collision) {
        // Abort if we already attacked recently.
        if (Time.time - _lastAttackTime < attackCooldown) return;

        // CompareTag is cheaper than .tag ==
        if(collision.gameObject.CompareTag("Player")) {
            playerHealth.TakeDamage(damage);

            // Remember that we recently attacked.
            _lastAttackTime = Time.time;
        }
    }
}

The advantage here is that we don't have to allocate and construct a new Coroutine instance. Just updating a float is dirt cheap. Subtracting float timestamps is not entirely precise, but for values that don't need sub-millisecond precision like an attack cooldown on the order of a half-second or more, this is "good enough".

But if you really want to do it with a coroutine, you can do it like this:

public class EnemyAttack : MonoBehaviour
{
    public int damage;
    public PlayerHealth playerHealth;
    public float attackCooldown;

    Coroutine _attackInProgress;
    int _playerContacts;

    private void OnCollisionEnter2D(Collision2D collision) {

        if(collision.gameObject.CompareTag("Player")) {
            _playerContacts++;
            if (_attackInProgress == null)
                 _attackInProgress = StartCoroutine(AttackLoop);
        }
    }

    private void OnCollisionExit2D(Collision2D collision) {

        if(collision.gameObject.CompareTag("Player")) {
            _playerContacts--;
        }
    }

    private IEnumerator AttackLoop() {
        while(_playerContacts > 0) {
            playerHealth.TakeDamage(damage);
    
            // You can construct this once and cache it if you like.
            yield return new WaitForSeconds(attackCooldown);
        }
        _attackInProgress = null;
    }
}

I've used the _playerContacts counter to exit the coroutine instead of calling StopCoroutine in OnCollisionExit for a couple of reasons:

  • It preserves the cooldown even if we briefly separate and re-collide, instead of short-circuiting and letting another attack through early.

  • It correctly handles cases where the player or enemy have multiple colliders (and so might get an exit message for one pair of colliders while another pair is still touching).

\$\endgroup\$
4
  • \$\begingroup\$ I'm so thankful for that. I struggled with that since yesterday morning. I was at this problem for hours. I fricking love you, I can't thank you enough. You're Great! \$\endgroup\$
    – Hammynator
    Commented Jan 7, 2023 at 18:43
  • 1
    \$\begingroup\$ I'm glad this was useful. If an answer solves your problem, you can click the ✔ icon in the top-left to mark it as "Accepted". \$\endgroup\$
    – DMGregory
    Commented Jan 7, 2023 at 19:06
  • \$\begingroup\$ Your edit to change the title, was misleading, when combined with the text of the question. if there is any discrepancy, I go with the text of the question. "2 seconds" was an important part of the original question title and should not have been reduced. \$\endgroup\$
    – user122973
    Commented Jan 8, 2023 at 5:10
  • \$\begingroup\$ @Strom you can always propose an edit of your own. \$\endgroup\$
    – DMGregory
    Commented Jan 8, 2023 at 13:14
0
\$\begingroup\$

Use the following code to match the one off code, no co-routines required.

Note the damage time is now in terms of seconds, so adjust the values accordingly. I suggest damage/600 as a starting point.

This change entirely removes the FPS dependency that your original code had, and makes the attack speed machine independent.

I would add some random variation to increase replay value, as indicated in the commented line (adjust values as needed):

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

public class EnemyAttack : MonoBehaviour
{
    [SerializeField] private GameObject Player;
    [SerializeField] private int timer;
    public int damagePerSecond;
    public PlayerHealth playerHealth;
    public float AttackCooldown;
    public float WaitForSecondsRealTime;
    float PlayerHealthh;

    private void OnCollisionStay2D(Collision2D collision) {
        if(collision.gameObject.CompareTag("Player")) { # attributed to DMGregory as faster.
            If (!player.Dodge) playerHealth.TakeDamage(Time.deltaTime * damagePerSecond);
//If (!player.Dodge) 
//   playerHealth.TakeDamage(Time.deltaTime * (damagePerSecond * Random.Range(.9f, 1f));
//else 
//   playerHealth.TakeDamage(Time.deltaTime * (damagePerSecond * Random.Range(.1f, .4f);
        }
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ "Periodically" means at regular intervals, like a beat, not continuously. Even lacking the 2 second guideline that was accidentally omitted from the title edit, this is still dealing damage every frame, which both the title and body text say is not wanted. \$\endgroup\$
    – DMGregory
    Commented Jan 8, 2023 at 13:15
  • \$\begingroup\$ @DMGregory The question body text alone begs for a time independent solution.(Please re-read it) This answer provides a possible solution, with explanation as to why the change failed., and would be helpful to anyone coming across this question with different expectations. I felt an apology was in order , with an explanation. I did not provide for the dodge, and neither did you. \$\endgroup\$
    – user122973
    Commented Jan 13, 2023 at 3:43
  • \$\begingroup\$ @Hammynator, I retract my earlier comment and contend that real-time dodge is important to your game mechanic, that adding a periodic delay will not solve. \$\endgroup\$
    – user122973
    Commented Jan 13, 2023 at 3:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .