英文:
How to register multiple instances "attacking" single enemy?
问题
以下是您提供的代码的翻译部分:
最近我尝试编写了一个脚本,其中敌人会受到多个实例的伤害。
让我感到困惑的是,该脚本不考虑其他实例的伤害。
我在敌人脚本中添加了冷却时间、伤害和攻击速度,以加快调试过程,但我无法弄清楚如何注册多个实例同时攻击。
我需要创建某种“战士”列表,其中列出了当前正在接触敌人并执行 foreach 循环吗?
请注意,代码部分没有翻译,只翻译了您提供的注释和文本。
英文:
I recently I tried to write a script in which enemy would be receiving damage by multiple instances.
What baffled me is that the script does not consider other instances damage
I added cooldown, damage and attack speed inside enemy script to speed up the debugging process, but I can't figure out how to register multiple instances attacking at the same time.
do i need to make some kind of a list of "Warriors" that are currently touching the enemy and do a foreach loop?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class HealthSystemScript : MonoBehaviour
{
[SerializeField]
private int AttackPower;
public int maxHealth = 100;
public int currentHealth;
private float _TimeSinceLastAttack = 10f;
private float attackCooldown = 5f;
public HealthBar HealthBar;
private void Start()
{
currentHealth = maxHealth;
HealthBar.SetMaxHealth(maxHealth);
}
void Update()
{
HealthBar.MaxHealth(currentHealth);
if (currentHealth <= 0 || Input.GetKey(KeyCode.F))
{
Destroy(gameObject);
}
}
private void OnCollisionStay2D(Collision2D collision)
{
//Check if enemy was hit by Warrior
if (collision.gameObject.tag == "Warrior" && Time.time > attackCooldown)
{
// Abort if we already attacked recently.
if (Time.time - _TimeSinceLastAttack < attackCooldown) return;
{
//if collided object has "Warrior" Tag then:
if (collision.gameObject.CompareTag("Warrior"))
{
TakeDamage(20);
Debug.Log("Boss took 20dmg!");
// Remember that we recently attacked.
_TimeSinceLastAttack = Time.time;
if (currentHealth <= 0 || Input.GetKey(KeyCode.F))
{
Destroy(gameObject);
}
}
}
}
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
HealthBar.SetHealth(currentHealth);
}
}
答案1
得分: 1
Here is the translated code portion:
> do i need to make somekind of a list of "Warriors" that are currently touching the enemy and do a foreach loop?
Yes!
`OnCollisionStay2D` is called for each individual touching collider yes, **however**, your entire code within it is purely time based!
Since you already re-assign
_TimeSinceLastAttack = Time.time;
for the first touching warrior the other calls to `OnCollisionStay2D` for the remaining warriors won't pass the
if (Time.time - _TimeSinceLastAttack < attackCooldown) return;
in that same frame anymore!
---
If I understood correctly you want to track individual attack times for each warrior so I would rather do something like e.g.
public class HealthSystemScript : MonoBehaviour
{
[SerializeField]
private int AttackPower;
public int maxHealth = 100;
public int currentHealth;
public HealthBar HealthBar;
private float attackCooldown = 5f;
// maps each attacking warrior to its individual last attack time
private readonly Dictionary<GameObject, float> _lastAttackTimes = new();
private void Start()
{
currentHealth = maxHealth;
HealthBar.SetMaxHealth(maxHealth);
}
private void OnCollisionStay(Collision2D collision)
{
// Check if enemy was hit by Warrior -> no -> ignore
if (!collision.gameObject.CompareTag("Warrior")) return;
// This means either this didn't touch before or the previous attack is too recent -> ignore
if(_lastAttackTimes.TryGetValue(colision.gameObject, out var lastAttackTime) && Time.time - lastAttackTime > attackCooldown) return;
// otherwise inflict damage once now and track last attack time for this warrior
TakeDamage(20);
Debug.Log("Boss took 20dmg!");
_lastAttackTimes[colision.gameObject] = Time.time;
}
private void OnCollisionExit2D(Collision2D collision)
{
//Check if enemy was hit by Warrior
if (!collision.gameObject.CompareTag("Warrior")) return;
// remove from the map of warriors so next first touch is applied immediately
_lastAttackTimes.Remove(collision.gameObject);
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
// by moving this check here you can be sure i is always applied when receiving damage
// and you don't need to poll check anything each frame
if (currentHealth <= 0)
{
Destroy(gameObject);
}
else
{
HealthBar.SetHealth(currentHealth);
}
}
}
---
if you rather want to just have one single timer for all warriors combined it gets a bit simpler:
public class HealthSystemScript : MonoBehaviour
{
[SerializeField]
private int AttackPower;
public int maxHealth = 100;
public int currentHealth;
public HealthBar HealthBar;
private float attackCooldown = 5f;
private int touchingWarriorsAmount;
private float lastAttackTime;
private void Start()
{
currentHealth = maxHealth;
HealthBar.SetMaxHealth(maxHealth);
}
private void FixedUpdate()
{
// run one shared timer for all attacks
if (Time.time - lastAttackTime > attackCooldown)
{
TakeDamage(20 * touchingWarriorsAmount);
lastAttackTime = Time.time;
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
//Check if enemy was hit by Warrior
if (!collision.gameObject.CompareTag("Warrior")) return;
// simply increase the counter on enter
touchingWarriorsAmount++;
}
private void OnCollisionExit2D(Collision2D collision)
{
//Check if enemy was hit by Warrior
if (!collision.gameObject.CompareTag("Warrior")) return;
// simply decrease the counter on exit
touchingWarriorsAmount--;
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
if (currentHealth <= 0)
{
Destroy(gameObject);
}
else
{
HealthBar.SetHealth(currentHealth);
}
}
}
if each warrior has different damage power later (just a guess due to the `AttackPower`) you again might want to use a list and iterate over them of course but still do the time check only once - as said depends on your needs
---
In general I would rather go the other way round and instead of checking collisions and receive damage I would rather make the attacker the responsible/driving instance and inflict damage on the thing it is colliding with. This way it would be more trivial to have those attack intervals.
英文:
> do i need to make somekind of a list of "Warriors" that are currently touching the enemy and do a foreach loop?
Yes!
OnCollisionStay2D
is called for each individual touching collider yes, however, your entire code within it is purely time based!
Since you already re-assign
_TimeSinceLastAttack = Time.time;
for the first touching warrior the other calls to OnCollisionStay2D
for the remaining warriors won't pass the
if (Time.time - _TimeSinceLastAttack < attackCooldown) return;
in that same frame anymore!
If I understood correctly you want to track individual attack times for each warrior so I would rather do something like e.g.
public class HealthSystemScript : MonoBehaviour
{
[SerializeField]
private int AttackPower;
public int maxHealth = 100;
public int currentHealth;
public HealthBar HealthBar;
private float attackCooldown = 5f;
// maps each attacking warrior to its individual last attack time
private readonly Dictionary<GameObject, float> _lastAttackTimes = new();
private void Start()
{
currentHealth = maxHealth;
HealthBar.SetMaxHealth(maxHealth);
}
private void OnCollisionStay(Collision2D collision)
{
// Check if enemy was hit by Warrior -> no -> ignore
if (!collision.gameObject.CompareTag("Warrior")) return;
// This means either this didn't touch before or the previous attack is too recent -> ignore
if(_lastAttackTimes.TryGetValue(colision.gameObject, out var lastAttackTime) && Time.time - lastAttackTime > attackCooldown) return;
// otherwise inflict damage once now and track last attack time for this warrior
TakeDamage(20);
Debug.Log("Boss took 20dmg!");
_lastAttackTimes[colision.gameObject] = Time.time;
}
private void OnCollisionExit2D(Collision2D collision)
{
//Check if enemy was hit by Warrior
if (!collision.gameObject.CompareTag("Warrior")) return;
// remove from the map of warriors so next first touch is applied immediately
_lastAttackTimes.Remove(collision.gameObject);
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
// by moving this check here you can be sure i is always applied when receiving damage
// and you don't need to poll check anything each frame
if (currentHealth <= 0)
{
Destroy(gameObject);
}
else
{
HealthBar.SetHealth(currentHealth);
}
}
}
if you rather want to just have one single timer for all warriors combined it gets a bit simpler:
public class HealthSystemScript : MonoBehaviour
{
[SerializeField]
private int AttackPower;
public int maxHealth = 100;
public int currentHealth;
public HealthBar HealthBar;
private float attackCooldown = 5f;
private int touchingWarriorsAmount;
private float lastAttackTime;
private void Start()
{
currentHealth = maxHealth;
HealthBar.SetMaxHealth(maxHealth);
}
private void FixedUpdate()
{
// run one shared timer for all attacks
if (Time.time - lastAttackTime > attackCooldown)
{
TakeDamage(20 * touchingWarriorsAmount);
lastAttackTime = Time.time;
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
//Check if enemy was hit by Warrior
if (!collision.gameObject.CompareTag("Warrior")) return;
// simply increase the counter on enter
touchingWarriorsAmount++;
}
private void OnCollisionExit2D(Collision2D collision)
{
//Check if enemy was hit by Warrior
if (!collision.gameObject.CompareTag("Warrior")) return;
// simply decrease the counter on exit
touchingWarriorsAmount--;
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
if (currentHealth <= 0)
{
Destroy(gameObject);
}
else
{
HealthBar.SetHealth(currentHealth);
}
}
}
if each warrior has different damage power later (just a guess due to the AttackPower
) you again might want to use a list and iterate over them of course but still do the time check only once - as said depends on your needs
In general I would rather go the other way round and instead of checking collisions and receive damage I would rather make the attacker the responsible/driving instance and inflict damage on the thing it is colliding with. This way it would be more trivial to have those attack intervals.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论