游戏开发中的状态管理与逻辑引擎设计:以Unity飞行棋为例

📅 2026/8/25 20:21:55
游戏开发中的状态管理与逻辑引擎设计:以Unity飞行棋为例
你花了三天时间终于把飞行棋的棋盘、棋子、骰子动画都做出来了。看着棋子一格一格地跳动骰子在屏幕上旋转心里满是成就感。你兴奋地点击“开始游戏”准备和AI大战三百回合。然后游戏卡住了。不是崩溃而是逻辑上的卡壳——骰子点数明明是6棋子却停在原地该轮到玩家B了界面却还在等待玩家A操作或者更糟游戏直接进入了某个谁也赢不了的死循环。这大概是每个从零开始做游戏的开发者都会遇到的经典时刻视觉和交互的拼图完成了但驱动整个游戏运转的那套“规则引擎”还没装上。我们之前完成了棋盘绘制、棋子移动、动画效果这些“面子工程”现在到了最核心也最容易出错的“里子工程”游戏逻辑与状态管理。这部分代码不会直接显示在屏幕上却决定了游戏是能顺畅玩下去还是一碰就碎。很多人觉得游戏逻辑就是一堆if-else判断写起来很快。但真正做过完整项目的人会告诉你逻辑代码的难点从来不是“写出来”而是“理得清”和“改得动”。当玩家操作、AI行为、回合切换、胜负判定、规则校验比如“飞到终点必须掷出精确点数”全部纠缠在一起时代码会迅速变成一团乱麻。今天我们就来彻底解决这个问题为你的2D飞行棋项目构建一个清晰、健壮、可扩展的游戏逻辑内核。1. 先想清楚游戏状态到底是什么以及为什么需要专门管理它在动手写代码前我们得先达成一个共识“游戏状态”不是一个变量而是一个需要被精心设计的数据模型。它就像游戏的记忆中枢记录了在任意时刻游戏世界处于何种局面。对于飞行棋这个状态模型至少需要回答以下问题宏观层面游戏处于哪个阶段准备、进行中、已结束当前是谁的回合玩家A、玩家B、AI玩家层面每位玩家有多少架飞机每架飞机分别在棋盘上的哪个位置起点、航道上、终点哪些飞机处于“已起飞”状态回合层面本回合是否已掷骰子掷出的点数是多少玩家是否已选择用哪架飞机移动移动是否已完成规则层面是否有飞机被撞回起点是否有飞机恰好抵达奖励格跳转或惩罚格暂停是否触发了游戏结束条件如果你把这些信息零散地放在不同的bool、int、GameObject引用里很快你就会在代码里写满这样的语句if (isPlayerATurn diceRolled !isMoving selectedPlane ! null ...)。这不仅是阅读的噩梦更是BUG的温床。任何一处状态更新遗漏都可能导致逻辑错乱。因此我们的第一个核心任务是设计一个中心化的GameState类或结构体来统一管理这些状态。这个类不负责渲染不处理输入它只做一件事——忠实地记录游戏数据并提供安全的方法来修改它们。// GameState.cs - 游戏状态数据模型 using System; using System.Collections.Generic; [Serializable] // 方便后续可能需要序列化保存游戏 public class GameState { public enum GamePhase { Preparing, Playing, Finished } public enum PlayerType { PlayerA, PlayerB, AI } public GamePhase CurrentPhase GamePhase.Preparing; public PlayerType CurrentTurn PlayerType.PlayerA; public int CurrentDiceValue 0; public bool IsDiceRolledThisTurn false; public bool HasMovedThisTurn false; // 玩家状态使用字典或数组存储索引对应玩家 public PlayerState[] Players new PlayerState[2]; // 假设2人游戏 // 历史记录可选用于回放或调试 public ListGameAction ActionHistory new ListGameAction(); // 初始化游戏状态 public void InitializeNewGame() { CurrentPhase GamePhase.Playing; CurrentTurn PlayerType.PlayerA; CurrentDiceValue 0; IsDiceRolledThisTurn false; HasMovedThisTurn false; // 初始化玩家状态飞机都在起点 Players[0] new PlayerState(PlayerType.PlayerA); Players[1] new PlayerState(PlayerType.PlayerB); ActionHistory.Clear(); } // 切换到下一个回合 public void SwitchToNextTurn() { // 重置回合内状态 IsDiceRolledThisTurn false; HasMovedThisTurn false; CurrentDiceValue 0; // 切换当前玩家 CurrentTurn (CurrentTurn PlayerType.PlayerA) ? PlayerType.PlayerB : PlayerType.PlayerA; // 记录回合切换动作 ActionHistory.Add(new GameAction { ActionType TurnSwitch, Player CurrentTurn }); } // 检查游戏是否结束 public bool CheckGameOver(out PlayerType winner) { winner PlayerType.PlayerA; // 默认值 bool isOver false; // 规则所有飞机都抵达终点的玩家获胜 foreach (var player in Players) { if (player ! null player.AllPlanesAtDestination()) { winner player.Type; isOver true; break; } } if (isOver) { CurrentPhase GamePhase.Finished; } return isOver; } } // 玩家状态 [Serializable] public class PlayerState { public PlayerType Type; public PlaneState[] Planes new PlaneState[4]; // 假设每个玩家4架飞机 public PlayerState(PlayerType type) { Type type; for (int i 0; i Planes.Length; i) { Planes[i] new PlaneState(i); } } public bool AllPlanesAtDestination() { foreach (var plane in Planes) { if (plane.CurrentPosition.PositionType ! PositionType.Destination) return false; } return true; } } // 飞机状态 [Serializable] public class PlaneState { public int PlaneId; public BoardPosition CurrentPosition; // 包含格子索引、类型起点、航道、终点等 public bool IsActive false; // 是否已起飞 public PlaneState(int id) { PlaneId id; // 初始位置在起点 CurrentPosition new BoardPosition { PositionType PositionType.Start, Index id }; } }这个GameState类就是我们的“单一数据源”。UI显示谁回合、棋盘上棋子在哪、规则判断能不能移动全都以这里的值为准。状态改变必须通过定义好的方法如SwitchToNextTurn而不是直接修改变量字段这是避免状态混乱的第一道防线。2. 构建游戏逻辑引擎让规则自己说话而不是散落在代码各处有了状态模型接下来就需要一个“裁判”或“引擎”它根据当前状态和玩家输入决定接下来会发生什么并驱动状态更新。这就是我们的GameLogicController。这个控制器的核心职责是封装所有游戏规则。它的工作流非常清晰接收指令例如“玩家A掷骰子”、“玩家A移动1号飞机”。验证规则检查在当前状态下这个指令是否合法。比如不是你的回合不能掷骰子骰子没掷不能移动点数不够不能移动等。执行计算如果合法计算出指令的结果。例如移动后新的位置是否撞飞了别人的飞机是否触发了跳转等。更新状态将计算结果安全地更新到GameState中。通知世界告诉其他模块如UI控制器、动画控制器、音效控制器“状态已变更请更新”。// GameLogicController.cs - 游戏逻辑引擎 using UnityEngine; public class GameLogicController : MonoBehaviour { public GameState CurrentGameState; public UIManager UIManagerRef; public BoardManager BoardManagerRef; void Start() { CurrentGameState new GameState(); CurrentGameState.InitializeNewGame(); UIManagerRef.UpdateUI(CurrentGameState); // 初始化UI } // 外部调用的方法请求掷骰子 public void RequestRollDice(PlayerType requester) { // 1. 规则验证 if (CurrentGameState.CurrentPhase ! GameState.GamePhase.Playing) { Debug.LogWarning(游戏未开始或已结束不能掷骰子。); return; } if (CurrentGameState.CurrentTurn ! requester) { Debug.LogWarning($现在是 {CurrentGameState.CurrentTurn} 的回合{requester} 不能掷骰子。); return; } if (CurrentGameState.IsDiceRolledThisTurn) { Debug.LogWarning(本回合已经掷过骰子了。); return; } // 2. 执行核心逻辑生成随机点数1-6 int diceValue Random.Range(1, 7); CurrentGameState.CurrentDiceValue diceValue; CurrentGameState.IsDiceRolledThisTurn true; // 3. 记录动作 CurrentGameState.ActionHistory.Add(new GameAction { ActionType RollDice, Player requester, Value diceValue }); // 4. 特殊规则处理例如掷出6可以再掷一次或者可以起飞一架飞机 bool canRollAgain (diceValue 6); // 简化规则 // ... 根据规则更新状态 // 5. 通知其他系统 UIManagerRef.OnDiceRolled(diceValue, canRollAgain); // 播放骰子动画音效... Debug.Log(${requester} 掷出了 {diceValue}); } // 外部调用的方法请求移动飞机 public void RequestMovePlane(PlayerType requester, int planeId) { // 类似的验证链... // 1. 验证回合、玩家、是否已掷骰子、是否已移动 // 2. 验证这架飞机是否属于该玩家且状态可移动如已起飞 // 3. 根据骰子点数和飞机当前位置计算目标位置 // 4. 检查目标位置是否有其他飞机撞飞规则 // 5. 检查目标位置是否是特殊格跳转、暂停 // 6. 更新 GameState 中该飞机的位置 // 7. 检查是否抵达终点 // 8. 判断本回合是否结束如果是切换到下一回合 // 9. 检查游戏是否结束 // 10. 通知UI和动画系统更新 // 这是一个非常核心且复杂的方法需要耐心实现所有规则分支。 } // AI回合的简单逻辑示例 public void ProcessAITurn() { if (CurrentGameState.CurrentTurn ! GameState.PlayerType.AI) return; // 简单AI掷骰子然后选择第一架可移动的飞机移动 RequestRollDice(GameState.PlayerType.AI); // 等待骰子动画结束可以通过回调或协程 StartCoroutine(AIMoveAfterDelay(1.0f)); } System.Collections.IEnumerator AIMoveAfterDelay(float delay) { yield return new WaitForSeconds(delay); // 查找AI玩家可移动的飞机 var aiPlayerState CurrentGameState.Players[1]; // 假设索引1是AI foreach (var plane in aiPlayerState.Planes) { if (IsPlaneMovable(aiPlayerState.Type, plane.PlaneId)) { RequestMovePlane(GameState.PlayerType.AI, plane.PlaneId); break; } } // 如果没有飞机可移动直接结束回合 if (!CurrentGameState.HasMovedThisTurn) { CurrentGameState.SwitchToNextTurn(); UIManagerRef.UpdateUI(CurrentGameState); } } private bool IsPlaneMovable(PlayerType player, int planeId) { /* 实现移动性判断 */ return true; } }关键设计思想GameLogicController是游戏规则的唯一执行者。UI上的按钮点击、AI的决策最终都转化为对GameLogicController的方法调用如RequestRollDice。它负责所有“能不能做”和“做了会怎样”的判断确保规则的一致性。3. 打通状态、逻辑与表现层用事件驱动代替紧密耦合现在我们有GameState数据和GameLogicController规则还需要让棋盘上的棋子、UI上的文字、动画和音效跟着状态变化。最糟糕的做法是让GameLogicController直接持有并操作BoardManager、PlaneController、UIManager的所有引用然后一个个去调用它们的更新方法。这会导致代码高度耦合难以维护和测试。更优雅的方式是采用事件驱动Event-driven或观察者模式Observer Pattern。核心思想是GameLogicController在状态发生重要变化时发出一个“事件”或“消息”。其他所有关心这个变化的模块观察者自己来订阅这个事件并在事件发生时执行自己的更新逻辑。在Unity中我们可以使用C#的event委托或者Unity自带的UnityEvent甚至是一些消息系统框架。这里我们用C#的event做一个简单示例// GameEvents.cs - 定义游戏中所有可能的事件 public static class GameEvents { // 当游戏状态初始化或重置时触发 public static event System.ActionGameState OnGameStateInitialized; public static void TriggerGameStateInitialized(GameState state) OnGameStateInitialized?.Invoke(state); // 当骰子点数产生时触发参数点数是否可再掷 public static event System.Actionint, bool OnDiceRolled; public static void TriggerDiceRolled(int value, bool canRollAgain) OnDiceRolled?.Invoke(value, canRollAgain); // 当回合切换时触发参数新回合的玩家 public static event System.ActionGameState.PlayerType OnTurnSwitched; public static void TriggerTurnSwitched(GameState.PlayerType player) OnTurnSwitched?.Invoke(player); // 当飞机位置更新时触发参数玩家飞机ID旧位置新位置 public static event System.ActionGameState.PlayerType, int, BoardPosition, BoardPosition OnPlanePositionUpdated; public static void TriggerPlanePositionUpdated(GameState.PlayerType player, int planeId, BoardPosition from, BoardPosition to) OnPlanePositionUpdated?.Invoke(player, planeId, from, to); // 当游戏结束时触发参数获胜者 public static event System.ActionGameState.PlayerType OnGameOver; public static void TriggerGameOver(GameState.PlayerType winner) OnGameOver?.Invoke(winner); }然后修改GameLogicController在关键节点触发事件// 在GameLogicController的RequestRollDice方法中替换直接调用UIManager // UIManagerRef.OnDiceRolled(diceValue, canRollAgain); // 旧方式 GameEvents.TriggerDiceRolled(diceValue, canRollAgain); // 新方式触发事件 // 在SwitchToNextTurn或类似方法中 GameEvents.TriggerTurnSwitched(CurrentGameState.CurrentTurn); // 在成功移动飞机后 GameEvents.TriggerPlanePositionUpdated(requester, planeId, oldPos, newPos);最后让各个表现层模块在初始化时订阅它们关心的事件// UIManager.cs void OnEnable() { GameEvents.OnDiceRolled UpdateDiceUI; GameEvents.OnTurnSwitched UpdateTurnIndicatorUI; GameEvents.OnGameOver ShowGameOverPanel; } void OnDisable() { GameEvents.OnDiceRolled - UpdateDiceUI; GameEvents.OnTurnSwitched - UpdateTurnIndicatorUI; GameEvents.OnGameOver - ShowGameOverPanel; } private void UpdateDiceUI(int value, bool canRollAgain) { diceText.text value.ToString(); // ... 其他UI更新 }// PlaneController.cs (控制单个棋子视觉的脚本) void Start() { GameEvents.OnPlanePositionUpdated HandlePlanePositionUpdated; } private void HandlePlanePositionUpdated(GameState.PlayerType player, int planeId, BoardPosition from, BoardPosition to) { if (this.ownerPlayer player this.planeId planeId) { // 触发移动到新位置的动画 StartCoroutine(MoveToPositionOnBoard(to)); } }这样做的好处是巨大的解耦GameLogicController完全不知道UIManager或PlaneController的存在它只负责触发事件。添加新的视觉特效或声音模块时只需让新模块订阅事件即可无需修改核心逻辑。可测试性你可以单独测试GameLogicController的逻辑只需模拟事件触发看GameState是否正确更新而无需启动完整的Unity场景。清晰的数据流状态变化从逻辑核心发出像广播一样通知所有相关方数据流是单向且清晰的。4. 从“能玩”到“好玩”加入AI、音效、存档与优化当核心逻辑闭环跑通后你的游戏已经“可玩”了。但要让它成为一个完整的作品还需要最后几步打磨。4.1 实现一个简单的游戏AI一个最基本的飞行棋AI可以遵循以下决策树掷骰子。判断优先级优先级1是否有飞机可以恰好抵达终点有则移动它。优先级2是否有飞机可以撞飞对手的飞机有则移动它攻击性策略。优先级3是否有未起飞的飞机且掷出6或5/6有则起飞它。优先级4选择移动步数最远离终点最近的那架飞机。优先级5随机选择一架可移动的飞机。执行移动。判断是否连续回合如掷出6可再掷如果是回到第1步。你可以将这套决策逻辑实现在一个AIDecisionMaker类中由GameLogicController在AI回合调用。关键是把AI的“思考”过程也封装好并通过RequestMovePlane这个统一接口来行动保持规则执行路径的唯一性。4.2 集成音效与粒子特效音效和特效是游戏的“皮肤”能极大提升体验。继续利用事件系统在GameEvents中增加OnPlaneTakeOff、OnPlaneCollide、OnSpecialCellActivated等事件。创建一个AudioManager和VFXManager订阅这些事件。当事件触发时播放对应的音效如骰子滚动声、棋子移动声、撞击声、胜利音乐和粒子特效如移动轨迹光点、撞击火花、胜利烟花。4.3 实现游戏存档与读档得益于我们之前将GameState设计为[Serializable]存档变得非常简单。你可以使用JsonUtility.ToJson将CurrentGameState序列化成JSON字符串然后使用PlayerPrefs或System.IO.File保存到本地。public void SaveGame(string saveName) { string jsonString JsonUtility.ToJson(CurrentGameState); PlayerPrefs.SetString(saveName, jsonString); PlayerPrefs.Save(); Debug.Log(游戏已保存); } public bool LoadGame(string saveName) { if (PlayerPrefs.HasKey(saveName)) { string jsonString PlayerPrefs.GetString(saveName); CurrentGameState JsonUtility.FromJsonGameState(jsonString); // 触发一个“游戏状态已加载”事件让所有系统根据新状态刷新 GameEvents.TriggerGameStateInitialized(CurrentGameState); return true; } return false; }注意直接序列化GameState可能无法保存对Unity对象如Texture2D,GameObject引用的引用这些通常需要单独处理。我们的GameState只保存核心数据位置、状态枚举等是可行的。4.4 性能与体验优化输入防抖在RequestRollDice和RequestMovePlane开始时检查一个isProcessing标志防止玩家快速连续点击导致逻辑错乱。动画协同使用Coroutine和WaitForSeconds或WaitUntil来让逻辑等待动画播放完毕再更新状态或进行下一步。例如移动飞机时先触发动画动画结束后再触发OnPlanePositionUpdated事件中的后续逻辑如检查是否触发新规则。代码重构定期回顾GameLogicController中的RequestMovePlane方法如果过于庞大可以将规则校验、移动计算、碰撞检测、特殊格处理等拆分成独立的私有方法保持主方法清晰。走到这一步你的飞行棋已经不再是一个简单的演示而是一个拥有完整状态管理、清晰逻辑分层、事件驱动通信、并具备AI、音效和存档功能的可交付项目。这个过程里最重要的收获不是学会了某个API而是理解了如何将一个复杂的、交互密集的软件游戏分解为数据、逻辑、表现三个相对独立的层次并用明确的协议事件让它们协同工作。这套架构思维远比实现飞行棋本身更有价值它能应用到几乎所有类型的游戏乃至交互式应用开发中。下次当你启动Unity开始构思新点子时不妨先问自己这个游戏的“状态”是什么它的“规则引擎”又该如何设计