Unity自动战斗原型中Team2恒先攻击致胜的问题解决咨询
解决2D自动对战游戏攻击顺序无随机性问题
我是Unity新手,正在开发一款类似《Mage and Monsters》的2D自动对战游戏原型。测试时发现,两队属性完全相同的单位中,Team2总是先发起攻击并获胜,毫无随机性。虽已确保所有单位同步初始化,但问题仍存在,现寻求实现攻击顺序随机性的方案。
现有代码
BaseUnit.cs
public class BaseUnit : MonoBehaviour { public UnitData unitData; public int baseHealth; public int health; public int level; public int damage; public float range; public float attackSpeed; public float movementSpeed; public Team myTeam; public bool IsReady = false; public bool IsPlaying = false; protected BaseUnit currentTarget = null; protected bool HasEnemy => currentTarget != null; protected bool IsInRange => currentTarget != null && Vector3.Distance(this.transform.position, currentTarget.transform.position) <= range; protected bool dead = false; protected bool canAttack = true; protected float waitBetweenAttack; void Start() { baseHealth = unitData.health; health = baseHealth; level = unitData.level; damage = unitData.damage; range = unitData.range; attackSpeed = unitData.attackSpeed; movementSpeed = unitData.movementSpeed; UnitManager.Instance.OnRoundStart += OnRoundStart; UnitManager.Instance.OnRoundEnd += OnRoundEnd; UnitManager.Instance.OnUnitDied += OnUnitDied; IsReady = true; } protected virtual void OnRoundStart() { Debug.Log(gameObject.name + " OnRoundStart!"); IsPlaying = true; } protected virtual void OnRoundEnd() { } protected virtual void OnUnitDied(BaseUnit diedUnit) { } protected void FindTarget() { var allEnemies = UnitManager.Instance.GetUnitsAgainst(myTeam); float minDistance = Mathf.Infinity; BaseUnit entity = null; foreach (BaseUnit be in allEnemies) { if (Vector3.Distance(be.transform.position, this.transform.position) <= minDistance) { minDistance = Vector3.Distance(be.transform.position, this.transform.position); entity = be; } } currentTarget = entity; } protected void GetInRange() { if (Mathf.Abs(Vector3.Distance(currentTarget.transform.position, this.transform.position)) > range) { var step = movementSpeed * Time.deltaTime; this.transform.position = Vector3.MoveTowards(this.transform.position, currentTarget.transform.position, step); } } public void TakeDamage(int incomingDamage) { // Maybe add damage type eventually baseHealth -= incomingDamage; if (baseHealth <= 0 && !dead) { dead = true; UnitManager.Instance.UnitDead(this); } } protected virtual void Attack() { if (!canAttack) { return; } waitBetweenAttack = 1 / attackSpeed; StartCoroutine(WaitCoroutine()); } IEnumerator WaitCoroutine() { canAttack = false; yield return new WaitForSeconds(waitBetweenAttack); canAttack = true; } }
MeleeUnit.cs
public class MeleeUnit : BaseUnit { protected override void OnRoundStart() { base.OnRoundStart(); Debug.Log("Melee OnRoundStart for " + gameObject.name); FindTarget(); } public void Update() { if (!IsPlaying) { return; } if (!HasEnemy) { FindTarget(); } if (IsInRange) { if (canAttack) { Attack(); currentTarget.TakeDamage(damage); } } } public void FixedUpdate() { if (!IsPlaying) { return; } if (!HasEnemy) { return; } if (!IsInRange) { GetInRange(); } } }
UnitManager.cs
public class UnitManager : Manager<UnitManager> { private string teamOneTag = "Team1"; private string teamTwoTag = "Team2"; public List<BaseUnit> teamOne = new List<BaseUnit>(); public List<BaseUnit> teamTwo = new List<BaseUnit>(); public bool IsPlaying = false; public Action OnRoundStart; public Action OnRoundEnd; public Action<BaseUnit> OnUnitDied; private void Start() { Debug.Log(gameObject.name + " starting"); PopulateTeam(teamOne, teamOneTag); PopulateTeam(teamTwo, teamTwoTag); StartCoroutine(WaitForAllObjectsToBeReady()); } private bool UnitsAreReady() { foreach (BaseUnit unit in teamOne) { if (!unit.IsReady) { return false; } } foreach (BaseUnit unit in teamTwo) { if (!unit.IsReady) { return false; } } return true; } IEnumerator WaitForAllObjectsToBeReady() { while (!UnitsAreReady()) { Debug.Log("Waiting for 0.5 seconds."); yield return new WaitForSeconds(0.5f); } Debug.Log("Invoking OnRoundStart."); OnRoundStart?.Invoke(); } public void UnitDead(BaseUnit unit) { teamOne.Remove(unit); teamTwo.Remove(unit); Debug.Log("Deleting " + unit.gameObject.name); OnUnitDied?.Invoke(unit); Destroy(unit.gameObject); } private void PopulateTeam(List<BaseUnit> team, string tag) { var teamUnits = GameObject.FindGameObjectsWithTag(tag); foreach (var unit in teamUnits) { team.Add(unit.GetComponent<BaseUnit>()); } } public List<BaseUnit> GetUnitsAgainst(Team against) { return against == Team.Team1 ? teamTwo : teamOne; } }
问题原因
当前所有单位初始canAttack状态都是true,且在OnRoundStart后同步进入Update循环。Unity的Update调用顺序依赖Hierarchy面板中的对象排列顺序,若Team2的单位排在Team1之前,就会先执行攻击逻辑,导致每次都是Team2先出手。同时固定的攻击冷却时间会让后续攻击也保持同步,完全没有随机性。
解决方案
1. 添加初始攻击随机延迟
给每个单位添加随机的初始攻击延迟,确保第一轮攻击时间分散:
修改BaseUnit中的OnRoundStart方法,并新增一个协程:
protected virtual void OnRoundStart() { Debug.Log(gameObject.name + " OnRoundStart!"); IsPlaying = true; // 初始设置为不可攻击,随机延迟后再允许攻击 canAttack = false; // 0到0.5秒的随机延迟,可根据游戏节奏调整范围 float randomDelay = Random.Range(0f, 0.5f); StartCoroutine(InitialAttackDelay(randomDelay)); } IEnumerator InitialAttackDelay(float delay) { yield return new WaitForSeconds(delay); canAttack = true; }
2. 给攻击冷却添加随机波动
让每次攻击的间隔有小幅度随机变化,避免后续攻击再次同步:
修改BaseUnit中的Attack方法:
protected virtual void Attack() { if (!canAttack) { return; } // 给攻击冷却添加90%-110%的随机波动,保留原有攻击速度的同时增加随机性 waitBetweenAttack = (1 / attackSpeed) * Random.Range(0.9f, 1.1f); StartCoroutine(WaitCoroutine()); }
3. 优化攻击逻辑的耦合(可选)
当前MeleeUnit在Update中直接触发伤害,逻辑耦合度高。可以将伤害触发移到Attack方法的子类实现中,让逻辑更清晰:
首先修改BaseUnit的Attack方法,新增虚拟方法PerformAttack:
protected virtual void Attack() { if (!canAttack) { return; } waitBetweenAttack = (1 / attackSpeed) * Random.Range(0.9f, 1.1f); PerformAttack(); StartCoroutine(WaitCoroutine()); } // 子类重写该方法实现具体攻击逻辑 protected virtual void PerformAttack() { }
然后修改MeleeUnit的Update和重写PerformAttack:
public void Update() { if (!IsPlaying) { return; } if (!HasEnemy) { FindTarget(); } if (IsInRange && canAttack) { Attack(); } } protected override void PerformAttack() { currentTarget.TakeDamage(damage); }
这些修改后,两队单位的攻击顺序会变得随机,不会再出现固定Team2先获胜的情况。
内容的提问来源于stack exchange,提问作者Jake Alsemgeest
相关产品推荐
相关产品推荐

