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根源分析
- 随机数重复生成:每次调用
PerformEnemyAction都新创建System.Random实例,由于创建间隔极短,多个实例会使用相同的种子,生成完全一致的随机数。如果敌人数量多,会连续打出致命伤害,直接秒杀玩家。 - 回合循环逻辑缺陷:
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
相关产品推荐
相关产品推荐

