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

Unity协程实现回合制战斗:如何让角色依次行动并等待玩家输入?

Unity回合制战斗系统BUG修复:玩家未行动即被秒杀

问题描述

开发Unity回合制战斗系统,需求是所有角色按 initiative 依次行动,玩家回合接收用户输入。采用协程实现,但出现BUG:玩家尚未轮到行动,currentHealth直接降至0。尝试将PerformEnemyAction改为协程,问题仍未解决。

原代码

public enum CombatState { START, ENEMYTURN, PLAYERTURN, WON, LOST }

public class CombatSystem : MonoBehaviour
{
    public List<Character> currentEnemiesInCombat;
    public CombatState state;
    
    private GameController controller;

    private void Awake()
    {
        controller = GetComponent<GameController>();
    }

    //Need to create an action response similar to openhiddenroom that will 
    //activate combat upon coming across revealed enemies, then CombatSystem 
    //will handle what happens below
    public void StartCombat(PlayerCharacter playerCharacter, List<Character> triggeredEnemies)
    {
        //Bring up enemy illustrations, possibly resize canvas
        //Roll initiative for each character
        state = CombatState.START;
        controller.ResizeDisplayTextForCombat();
        controller.LogStringWithReturn("CombatSystem.cs DEBUG: YOU ARE IN COMBAT NOW");
        SetUpCharacterInitiatives(playerCharacter, triggeredEnemies);
        StartCoroutine(TurnBasedCombatRoutine(playerCharacter, triggeredEnemies));
    }

    //THIS NEEDS REWORKING FROM HERE DOWN-------------------------------------------------------------------------------------------------
    //------------------------------------------------------------------------------------------------------------------------------------
    IEnumerator TurnBasedCombatRoutine(PlayerCharacter playerCharacter, List<Character> triggeredEnemies)
    {
        var allCharactersInBattle = triggeredEnemies.Concat(new[] { playerCharacter }).OrderByDescending(c => c.initiative);

        //Keep the fight going while the player is above 0 hp and enemies still remain
        while (playerCharacter.currentHealth > 0 && triggeredEnemies.Count > 0)
        {
            foreach (Character character in allCharactersInBattle)
            {
                //Check if the character and playercharacter are still alive
                //because we don't want enemies taking more turnns if the player is
                //already dead
                if (character.currentHealth > 0 && playerCharacter.currentHealth > 0)
                {
                    controller.LogStringWithReturn("CombatSystem.cs DEBUG: " + character.charName + "'s turn");

                    //Perform actions for the current character's turn
                    //yield if it is the player's turn
                    if (character != controller.playerCharacter)
                    {
                        state = CombatState.ENEMYTURN;
                        PerformEnemyAction(character);
                    }
                    else
                    {
                        //NEED TO FIGURE OUT HOW TO MAKE THIS YIELD UNTIL A VALID USER INPUT
                        state = CombatState.PLAYERTURN;
                        controller.LogStringWithReturn("It is your turn...");
                        yield return null;
                    }

                    // Check for defeated enemies after each turn
                    triggeredEnemies.RemoveAll(enemy => isDead(enemy));

                    // Break out of the loop if all enemies are defeated
                    if (triggeredEnemies.Count == 0)
                    {
                        state = CombatState.WON;
                        controller.LogStringWithReturn("You have defeated all of your foes. It is safe to move forward.");
                        break;
                    }
                }
            }
        }

        EndCombat();
    }

    //Roll initiatives for all characters, including player
    //Make sure they don't have the same initiative amounts
    void SetUpCharacterInitiatives(PlayerCharacter playerCharacter, List<Character> CharactersInBattle)
    {
        System.Random rnd = new System.Random();
        HashSet<int> usedInitiatives = new HashSet<int>();

        // Set up playerCharacter as the first item in the CharactersInBattle List
        playerCharacter.initiative = rnd.Next(1, 21);
        usedInitiatives.Add(playerCharacter.initiative);

        // Set up initiatives for other characters
        for (int i = 0; i < CharactersInBattle.Count; i++)
        {
            int initiative;
            do
            {
                initiative = rnd.Next(1, 21);
            } while (usedInitiatives.Contains(initiative));

            CharactersInBattle[i].initiative = initiative;
            usedInitiatives.Add(initiative);
        }
    }

    void PerformEnemyAction(Character currentEnemy)
    {
        System.Random rnd = new System.Random();
        int attackRoll = rnd.Next(1, 21);
        int damageRoll = rnd.Next(1, currentEnemy.maxDamage);

        //if currentcharacter != playerCharacter, then make an attack roll
        //against playerCharacter's ArmorClass
        if (attackRoll > controller.playerCharacter.armorClass)
        {
            controller.playerCharacter.currentHealth = controller.playerCharacter.currentHealth - damageRoll;
            controller.LogStringWithReturn($"{currentEnemy.charName} hits you [{attackRoll}] for {damageRoll} damage!");
            //Check if player died from that hit
            if(isDead(controller.playerCharacter))
            {
                state = CombatState.LOST;
                controller.LogStringWithReturn("The Eternal Night washes over you... \nYOU ARE DEAD\nGAME OVER");
                GameOver();
            }
        }
        else if (attackRoll < controller.playerCharacter.armorClass)
        {
            controller.LogStringWithReturn($"{currentEnemy.charName} misses its attack!");
        }
        else
        {
            controller.LogStringWithReturn($"You narrowly dodge {currentEnemy.charName}'s attack!");
        }
    }

    bool isDead(Character character)
    {
        if (character.currentHealth <= 0)
        {
            return true;
        }
        else 
        {   
            return false; 
        }
    }

    void GameOver()
    {
        //Need to have something happen if you lose combat
        //Disable text input or allow a restart command
        //Maybe send them to a game over room???
    }

    void EndCombat()
    {
        controller.ResizeDisplayTextPostCombat();
    }
}

BUG根源分析

  1. 随机数重复生成:每次调用PerformEnemyAction都新创建System.Random实例,由于创建间隔极短,多个实例会使用相同的种子,生成完全一致的随机数。如果敌人数量多,会连续打出致命伤害,直接秒杀玩家。
  2. 回合循环逻辑缺陷:
    • allCharactersInBattle是一次性生成的枚举集合,当敌人被移除后,循环仍会遍历原集合中的已死敌人,导致重复攻击。
    • 玩家回合仅yield return null,仅等待一帧就继续循环,未真正等待用户输入完成,敌人会连续多轮行动。

修复方案

1. 统一随机数实例

将Random实例改为类级别字段,仅初始化一次:

private System.Random rnd;

private void Awake()
{
    controller = GetComponent<GameController>();
    rnd = new System.Random(); // 全局唯一随机数实例
}

修改PerformEnemyAction,使用全局随机数:

void PerformEnemyAction(Character currentEnemy)
{
    int attackRoll = rnd.Next(1, 21);
    int damageRoll = rnd.Next(1, currentEnemy.maxDamage);
    // 原有攻击逻辑不变
}

2. 修正回合循环逻辑

  • 每轮重新生成角色列表,确保移除的敌人不会被遍历:
IEnumerator TurnBasedCombatRoutine(PlayerCharacter playerCharacter, List<Character> triggeredEnemies)
{
    while (playerCharacter.currentHealth > 0 && triggeredEnemies.Count > 0)
    {
        // 每轮重新生成排序后的角色列表
        var allCharactersInBattle = triggeredEnemies.Concat(new[] { playerCharacter })
                                                   .OrderByDescending(c => c.initiative)
                                                   .ToList();
        foreach (Character character in allCharactersInBattle)
        {
            if (character.currentHealth > 0 && playerCharacter.currentHealth > 0)
            {
                controller.LogStringWithReturn("CombatSystem.cs DEBUG: " + character.charName + "'s turn");

                if (character != controller.playerCharacter)
                {
                    state = CombatState.ENEMYTURN;
                    PerformEnemyAction(character);
                }
                else
                {
                    state = CombatState.PLAYERTURN;
                    controller.LogStringWithReturn("It is your turn...");
                    // 等待玩家输入完成
                    bool playerActionDone = false;
                    controller.OnPlayerActionCompleted += () => playerActionDone = true;
                    yield return new WaitUntil(() => playerActionDone);
                    controller.OnPlayerActionCompleted -= () => playerActionDone = true;
                }

                triggeredEnemies.RemoveAll(enemy => isDead(enemy));

                if (triggeredEnemies.Count == 0)
                {
                    state = CombatState.WON;
                    controller.LogStringWithReturn("You have defeated all of your foes. It is safe to move forward.");
                    break;
                }
            }
        }
    }

    EndCombat();
}
  • 在GameController中添加玩家动作完成事件,用于通知协程:
public event Action OnPlayerActionCompleted;

// 当玩家完成动作(如攻击、技能)时调用此方法
public void NotifyPlayerActionCompleted()
{
    OnPlayerActionCompleted?.Invoke();
}

3. 可选:敌人动作添加延迟(提升体验)

将PerformEnemyAction改为协程,添加动作延迟,让回合流程更自然:

IEnumerator PerformEnemyAction(Character currentEnemy)
{
    yield return new WaitForSeconds(1f); // 模拟敌人动作延迟
    int attackRoll = rnd.Next(1, 21);
    int damageRoll = rnd.Next(1, currentEnemy.maxDamage);

    if (attackRoll > controller.playerCharacter.armorClass)
    {
        controller.playerCharacter.currentHealth -= damageRoll;
        controller.LogStringWithReturn($"{currentEnemy.charName} hits you [{attackRoll}] for {damageRoll} damage!");
        if(isDead(controller.playerCharacter))
        {
            state = CombatState.LOST;
            controller.LogStringWithReturn("The Eternal Night washes over you... \nYOU ARE DEAD\nGAME OVER");
            GameOver();
        }
    }
    else if (attackRoll < controller.playerCharacter.armorClass)
    {
        controller.LogStringWithReturn($"{currentEnemy.charName} misses its attack!");
    }
    else
    {
        controller.LogStringWithReturn($"You narrowly dodge {currentEnemy.charName}'s attack!");
    }
}

调用时需等待协程完成:

if (character != controller.playerCharacter)
{
    state = CombatState.ENEMYTURN;
    yield return StartCoroutine(PerformEnemyAction(character));
}

内容的提问来源于stack exchange,提问作者Terrible-Coder6776

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 09:07:02