You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 03:17:09