ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Unity六边形网格游戏开发:从地形生成到动态交互与性能优化

Unity六边形网格游戏开发:从地形生成到动态交互与性能优化 在上一篇文章中我们完成了六边形星球生成引擎的核心算法与基础地形构建。如果你还没看过建议先阅读上篇了解噪声生成、六边形网格构建和基础地形着色。本篇我们将深入游戏开发的下半场聚焦于如何将静态的六边形星球“激活”实现动态交互、资源系统、UI界面以及性能优化最终打造一个可玩性高的游戏原型。本文适合已经掌握Unity和C#基础并对程序化生成或策略/模拟类游戏开发感兴趣的开发者。通过本篇你将学会如何为程序化生成的地形注入“灵魂”构建一套完整的游戏循环。1. 从静态地形到动态世界游戏逻辑的注入一个只有地形的星球是静态的、无趣的。游戏的核心在于交互与变化。我们需要为每个六边形单元Hex Cell赋予状态和行为并建立一套驱动世界运转的规则系统。1.1 扩展六边形单元数据模型首先我们需要大幅扩展HexCell类的定义。它不再仅仅是一个存储位置和高度信息的容器而应成为一个承载游戏逻辑的最小单元。// 文件路径Assets/Scripts/Hex/HexCell.cs using UnityEngine; public class HexCell : MonoBehaviour { // 基础地理信息 public HexCoordinates coordinates; public float Elevation; // 海拔高度 public float Moisture; // 湿度用于决定植被、河流 public float Temperature; // 温度影响生物群落 public HexTerrainType TerrainType; // 地形类型枚举 // 游戏逻辑属性 public int OwnerId -1; // 所属玩家ID-1表示无主 public ResourceStack Resources; // 当前资源存量 public Unit OccupyingUnit; // 当前驻扎的单位 public Building ConstructedBuilding; // 当前建造的建筑 public bool IsExplored false; // 是否被探索战争迷雾 public bool IsVisible false; // 当前是否可见 // 邻接关系 [SerializeField] private HexCell[] neighbors new HexCell[6]; // 可视化组件 private MeshRenderer meshRenderer; private Color baseColor; void Awake() { meshRenderer GetComponentInChildrenMeshRenderer(); if (meshRenderer ! null) { baseColor meshRenderer.material.color; } } // 获取/设置邻居 public HexCell GetNeighbor(HexDirection direction) { return neighbors[(int)direction]; } public void SetNeighbor(HexDirection direction, HexCell cell) { neighbors[(int)direction] cell; cell.neighbors[(int)direction.Opposite()] this; } // 根据游戏状态更新外观如高亮、颜色变化 public void UpdateVisuals() { if (meshRenderer null) return; Color finalColor baseColor; // 高亮选中状态 if (IsSelected) { finalColor Color.Lerp(finalColor, Color.yellow, 0.5f); } // 显示可移动范围 else if (IsInMovementRange) { finalColor Color.Lerp(finalColor, Color.cyan, 0.3f); } // 战争迷雾效果 else if (!IsVisible) { finalColor Color.gray; } // 根据所有者着色 else if (OwnerId 0) { finalColor Color.Lerp(finalColor, GameManager.Instance.GetPlayerColor(OwnerId), 0.2f); } meshRenderer.material.color finalColor; } // 属性变更事件简化示例 public bool IsSelected { get; set; } public bool IsInMovementRange { get; set; } } // 地形类型枚举 public enum HexTerrainType { Ocean, Coast, Plains, Forest, Hills, Mountains, Desert, Tundra, Lake, River } // 资源堆结构体 [System.Serializable] public struct ResourceStack { public int Food; public int Production; public int Gold; public int Science; // ... 可以扩展其他资源 }1.2 实现回合制游戏管理器对于策略游戏一个中央化的GameManager是必不可少的。它负责管理游戏状态、玩家顺序、回合逻辑和全局规则。// 文件路径Assets/Scripts/Managers/GameManager.cs using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance; // 单例模式便于访问 public HexGrid HexGrid; // 引用我们的六边形网格 public ListPlayer Players new ListPlayer(); public int CurrentPlayerIndex 0; public bool IsGamePaused false; // 游戏状态事件可用于UI更新 public delegate void GameStateHandler(); public event GameStateHandler OnTurnBegin; public event GameStateHandler OnTurnEnd; public event GameStateHandler OnGameOver; void Awake() { if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); // 跨场景保持 } else { Destroy(gameObject); } } void Start() { InitializeGame(); } void InitializeGame() { // 1. 初始化玩家示例两个玩家 Players.Add(new Player(0, Player 1, Color.blue)); Players.Add(new Player(1, Player 2, Color.red)); // 2. 为玩家分配起始位置寻找合适的陆地单元格 foreach (var player in Players) { HexCell startCell FindStartCellForPlayer(player); if (startCell ! null) { startCell.OwnerId player.Id; startCell.IsExplored true; startCell.IsVisible true; // 可以在这里生成起始单位或建筑 SpawnStartingUnit(startCell, player); } } // 3. 开始第一个玩家的回合 BeginTurn(Players[CurrentPlayerIndex]); } // 寻找起始单元格避免海洋寻找资源丰富的平原或草原 HexCell FindStartCellForPlayer(Player player) { // 这是一个简化示例。实际项目中可能需要更复杂的算法 // 如确保玩家间距离足够远起始地资源平衡等。 ListHexCell potentialCells new ListHexCell(); foreach (var cell in HexGrid.Cells) { if (cell.TerrainType ! HexTerrainType.Ocean cell.TerrainType ! HexTerrainType.Mountains cell.Elevation 0.1f) // 确保不是低洼海岸 { potentialCells.Add(cell); } } if (potentialCells.Count 0) { // 简单随机实际应加入平衡性考量 return potentialCells[Random.Range(0, potentialCells.Count)]; } return null; } void SpawnStartingUnit(HexCell cell, Player player) { // 实例化一个单位预制体并设置其属性 GameObject unitPrefab Resources.LoadGameObject(Prefabs/Units/Settler); if (unitPrefab ! null) { GameObject unitObj Instantiate(unitPrefab, cell.transform.position, Quaternion.identity); Unit unit unitObj.GetComponentUnit(); if (unit ! null) { unit.Initialize(cell, player.Id); cell.OccupyingUnit unit; player.Units.Add(unit); } } } public void BeginTurn(Player player) { Debug.Log($Player {player.Name}s turn begins.); IsGamePaused false; // 重置玩家单位的移动力等状态 foreach (var unit in player.Units) { unit.ResetTurn(); } // 计算玩家所有城市的资源产出 foreach (var city in player.Cities) { city.ProcessTurn(); } // 触发回合开始事件更新UI OnTurnBegin?.Invoke(); } public void EndTurn() { Player currentPlayer Players[CurrentPlayerIndex]; Debug.Log($Player {currentPlayer.Name}s turn ends.); // 触发回合结束事件 OnTurnEnd?.Invoke(); // 切换到下一个玩家 CurrentPlayerIndex (CurrentPlayerIndex 1) % Players.Count; BeginTurn(Players[CurrentPlayerIndex]); } // 提供给UI按钮调用 public void UI_EndTurnButton() { if (!IsGamePaused) { EndTurn(); } } public Color GetPlayerColor(int playerId) { foreach (var player in Players) { if (player.Id playerId) return player.Color; } return Color.white; } } // 玩家类 [System.Serializable] public class Player { public int Id; public string Name; public Color Color; public ListUnit Units new ListUnit(); public ListCity Cities new ListCity(); public ResourceStack TotalResources; public Player(int id, string name, Color color) { Id id; Name name; Color color; } }2. 单位与移动系统让世界动起来单位如 settlers, warriors, builders是玩家与游戏世界交互的直接媒介。我们需要一个灵活的单位系统。2.1 单位基类与移动逻辑// 文件路径Assets/Scripts/Units/Unit.cs using System.Collections.Generic; using UnityEngine; public class Unit : MonoBehaviour { public string UnitName; public int OwnerId; public int MovementRange 2; // 每回合移动力 public int CurrentMovement; public int Strength; // 战斗强度 protected HexCell currentCell; protected QueueHexCell pathToFollow; protected bool isMoving false; public HexCell CurrentCell { get { return currentCell; } set { if (currentCell ! null) { currentCell.OccupyingUnit null; } currentCell value; if (currentCell ! null) { currentCell.OccupyingUnit this; transform.position currentCell.transform.position Vector3.up * 0.5f; // 稍微抬升单位 } } } public void Initialize(HexCell startCell, int ownerId) { CurrentCell startCell; OwnerId ownerId; ResetTurn(); } public void ResetTurn() { CurrentMovement MovementRange; } // 计算并高亮可移动范围 public void ShowMovementRange() { if (CurrentCell null) return; // 使用广度优先搜索(BFS)计算移动范围 HashSetHexCell reachableCells new HashSetHexCell(); QueueHexCell frontier new QueueHexCell(); DictionaryHexCell, int moveCostToCell new DictionaryHexCell, int(); frontier.Enqueue(CurrentCell); moveCostToCell[CurrentCell] 0; while (frontier.Count 0) { HexCell current frontier.Dequeue(); int currentCost moveCostToCell[current]; for (HexDirection d HexDirection.NE; d HexDirection.NW; d) { HexCell neighbor current.GetNeighbor(d); if (neighbor null) continue; // 计算移动到邻居的成本地形影响 int moveCost GetMoveCost(current, neighbor); int newCost currentCost moveCost; // 如果移动成本在移动力范围内且未访问过或找到更优路径 if (newCost MovementRange (!moveCostToCell.ContainsKey(neighbor) || newCost moveCostToCell[neighbor])) { moveCostToCell[neighbor] newCost; frontier.Enqueue(neighbor); reachableCells.Add(neighbor); } } } // 高亮显示可移动单元格 foreach (var cell in reachableCells) { cell.IsInMovementRange true; cell.UpdateVisuals(); } // 同时高亮当前单元格 CurrentCell.IsSelected true; CurrentCell.UpdateVisuals(); } // 隐藏移动范围高亮 public void HideMovementRange() { // 遍历网格所有单元格重置高亮状态实际项目应优化只重置之前高亮的单元格 foreach (var cell in FindObjectOfTypeHexGrid().Cells) { cell.IsInMovementRange false; cell.IsSelected false; cell.UpdateVisuals(); } } // 根据地形类型计算移动成本 int GetMoveCost(HexCell from, HexCell to) { int baseCost 1; switch (to.TerrainType) { case HexTerrainType.Plains: case HexTerrainType.Desert: baseCost 1; break; case HexTerrainType.Forest: case HexTerrainType.Hills: baseCost 2; break; case HexTerrainType.Mountains: baseCost 3; // 某些单位可能无法进入 break; case HexTerrainType.River: case HexTerrainType.Ocean: baseCost 99; // 不可通行除非有船 break; default: baseCost 1; break; } // 可以考虑高度差带来的额外成本 float elevationDiff Mathf.Abs(to.Elevation - from.Elevation); if (elevationDiff 0.2f) baseCost 1; return baseCost; } // 移动到目标单元格直接瞬移简化版 public bool MoveTo(HexCell targetCell) { if (targetCell null || CurrentMovement 0) return false; if (targetCell.OccupyingUnit ! null targetCell.OccupyingUnit.OwnerId ! this.OwnerId) { // 触发战斗 return EngageCombat(targetCell); } // 简单计算成本实际应使用寻路算法如A* int cost GetMoveCost(CurrentCell, targetCell); if (cost CurrentMovement) { CurrentCell targetCell; CurrentMovement - cost; return true; } return false; } bool EngageCombat(HexCell targetCell) { Unit defender targetCell.OccupyingUnit; // 简单的战斗逻辑 if (this.Strength defender.Strength) { Destroy(defender.gameObject); CurrentCell targetCell; CurrentMovement 0; return true; } else { Destroy(this.gameObject); return false; } } }2.2 单位选择与输入控制我们需要一个InputController来处理玩家的点击选择单位并指挥其移动。// 文件路径Assets/Scripts/Managers/InputController.cs using UnityEngine; public class InputController : MonoBehaviour { public Camera MainCamera; public LayerMask HexCellLayer; // 只为六边形单元格设置的Layer private Unit selectedUnit null; void Update() { if (Input.GetMouseButtonDown(0)) // 左键点击 { HandleLeftClick(); } else if (Input.GetMouseButtonDown(1) selectedUnit ! null) // 右键命令 { HandleRightClick(); } else if (Input.GetKeyDown(KeyCode.Escape)) // 取消选择 { DeselectUnit(); } } void HandleLeftClick() { Ray ray MainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, Mathf.Infinity, HexCellLayer)) { HexCell clickedCell hit.collider.GetComponentInParentHexCell(); if (clickedCell ! null) { // 如果点击的单元格有单位且该单位属于当前玩家 if (clickedCell.OccupyingUnit ! null clickedCell.OccupyingUnit.OwnerId GameManager.Instance.Players[GameManager.Instance.CurrentPlayerIndex].Id) { SelectUnit(clickedCell.OccupyingUnit); } // 如果已经选择了一个单位则尝试移动 else if (selectedUnit ! null) { // 这里可以加入路径寻找A*的逻辑此处简化为直接移动 if (selectedUnit.MoveTo(clickedCell)) { // 移动后更新显示并可能取消选择 selectedUnit.HideMovementRange(); DeselectUnit(); } } else { // 点击空地取消当前选择 DeselectUnit(); } } } } void HandleRightClick() { // 右键可以设置路径点或取消移动 Ray ray MainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, Mathf.Infinity, HexCellLayer)) { HexCell clickedCell hit.collider.GetComponentInParentHexCell(); if (clickedCell ! null selectedUnit ! null) { // 示例右键直接移动与左键逻辑相同实际可能不同 if (selectedUnit.MoveTo(clickedCell)) { selectedUnit.HideMovementRange(); DeselectUnit(); } } } } void SelectUnit(Unit unit) { if (selectedUnit ! null) { selectedUnit.HideMovementRange(); } selectedUnit unit; selectedUnit.ShowMovementRange(); Debug.Log($Selected unit: {unit.UnitName}); // 这里可以触发UI更新显示单位状态 } void DeselectUnit() { if (selectedUnit ! null) { selectedUnit.HideMovementRange(); } selectedUnit null; Debug.Log(Unit deselected.); } }3. 资源、建筑与城市系统构建游戏经济循环资源是策略游戏的血液。我们需要建立一套资源产出、收集和消耗的循环。3.1 资源产出与单元格收益每个单元格根据其地形和上面的建筑每回合会产生资源。我们需要在HexCell或一个专门的YieldSystem中计算。// 文件路径Assets/Scripts/Economy/CellYieldCalculator.cs public static class CellYieldCalculator { public static ResourceStack CalculateYield(HexCell cell) { ResourceStack yield new ResourceStack(); // 基础地形产出 switch (cell.TerrainType) { case HexTerrainType.Plains: yield.Food 2; yield.Production 1; break; case HexTerrainType.Forest: yield.Food 1; yield.Production 2; break; case HexTerrainType.Hills: yield.Production 2; yield.Gold 1; break; case HexTerrainType.Desert: // 沙漠产出低 break; case HexTerrainType.Coast: yield.Food 1; yield.Gold 1; break; // ... 其他地形 } // 特殊资源如河流、资源点加成 if (cell.Moisture 0.7f) yield.Food 1; // 潮湿地块食物1 if (cell.Elevation 0.8f) yield.Production 1; // 高山生产1 // 建筑加成如果单元格上有建筑 if (cell.ConstructedBuilding ! null) { yield.Food cell.ConstructedBuilding.FoodYield; yield.Production cell.ConstructedBuilding.ProductionYield; yield.Gold cell.ConstructedBuilding.GoldYield; yield.Science cell.ConstructedBuilding.ScienceYield; } return yield; } }3.2 建筑与城市类城市是玩家发展的核心它管理着一定范围内的单元格城市边界并可以建造建筑来增强产出。// 文件路径Assets/Scripts/City/City.cs using System.Collections.Generic; using UnityEngine; public class City : MonoBehaviour { public string CityName; public int OwnerId; public HexCell LocationCell; // 城市中心单元格 public ListHexCell WorkedCells new ListHexCell(); // 城市正在利用的单元格 public ListBuilding ConstructedBuildings new ListBuilding(); public ResourceStack Stockpile; // 城市库存 public ResourceStack PerTurnYield; // 每回合产出 public Building CurrentProduction { get; private set; } // 当前正在建造的项目 public int ProductionProgress { get; private set; } // 建造进度 void Start() { // 初始化时城市自动获取相邻的几格作为可工作单元格 ClaimInitialCells(); RecalculateYield(); } void ClaimInitialCells() { WorkedCells.Clear(); WorkedCells.Add(LocationCell); // 城市中心格 // 添加第一环的邻居简化逻辑 for (HexDirection d HexDirection.NE; d HexDirection.NW; d) { HexCell neighbor LocationCell.GetNeighbor(d); if (neighbor ! null neighbor.TerrainType ! HexTerrainType.Ocean) { WorkedCells.Add(neighbor); } } } // 每回合调用处理资源产出和建造进度 public void ProcessTurn() { // 1. 收集资源 RecalculateYield(); Stockpile.Food PerTurnYield.Food; Stockpile.Production PerTurnYield.Production; Stockpile.Gold PerTurnYield.Gold; Stockpile.Science PerTurnYield.Science; // 2. 处理人口增长/饥饿简化 // 3. 处理建造进度 if (CurrentProduction ! null) { ProductionProgress PerTurnYield.Production; if (ProductionProgress CurrentProduction.ProductionCost) { CompleteProduction(); } } } void RecalculateYield() { PerTurnYield new ResourceStack(); foreach (var cell in WorkedCells) { ResourceStack cellYield CellYieldCalculator.CalculateYield(cell); PerTurnYield.Food cellYield.Food; PerTurnYield.Production cellYield.Production; PerTurnYield.Gold cellYield.Gold; PerTurnYield.Science cellYield.Science; } // 建筑提供的全局加成 foreach (var building in ConstructedBuildings) { PerTurnYield.Food building.FoodYield; PerTurnYield.Production building.ProductionYield; PerTurnYield.Gold building.GoldYield; PerTurnYield.Science building.ScienceYield; } } public void SetProduction(Building building) { if (building null) return; CurrentProduction building; ProductionProgress 0; Debug.Log(${CityName} started producing {building.BuildingName}.); } void CompleteProduction() { if (CurrentProduction null) return; // 将建筑添加到城市 ConstructedBuildings.Add(CurrentProduction); Debug.Log(${CityName} has completed {CurrentProduction.BuildingName}!); // 应用建筑效果例如如果是粮仓增加食物存储上限 ApplyBuildingEffect(CurrentProduction); // 重置生产队列 CurrentProduction null; ProductionProgress 0; } void ApplyBuildingEffect(Building building) { // 根据建筑类型应用效果 // 例如building.BuildingType BuildingType.Granary } } // 建筑数据类ScriptableObject 非常适合存储此类数据 // 文件路径Assets/Scripts/Building/Building.cs using UnityEngine; [CreateAssetMenu(fileName NewBuilding, menuName Hex Game/Building)] public class Building : ScriptableObject { public string BuildingName; public string Description; public int ProductionCost; // 所需生产力 public int MaintenanceCost; // 每回合维护费金币 // 产出加成 public int FoodYield; public int ProductionYield; public int GoldYield; public int ScienceYield; // 前置科技、所需地形等条件可以在这里添加 // public Technology RequiredTech; // public HexTerrainType[] AllowedTerrain; }4. 用户界面UI集成连接玩家与游戏世界一个清晰的UI是游戏可玩性的关键。我们将使用Unity的UGUI系统创建基础界面。4.1 游戏状态UI创建一个UIManager来管理所有UI元素的更新。// 文件路径Assets/Scripts/UI/UIManager.cs using TMPro; // 需要TextMeshPro包 using UnityEngine; using UnityEngine.UI; public class UIManager : MonoBehaviour { public static UIManager Instance; // 顶部状态栏 public TMP_Text TurnCounterText; public TMP_Text CurrentPlayerText; public TMP_Text ResourceFoodText; public TMP_Text ResourceProductionText; public TMP_Text ResourceGoldText; public TMP_Text ResourceScienceText; // 单位/城市信息面板 public GameObject SelectionPanel; public TMP_Text SelectionNameText; public TMP_Text SelectionDetailsText; public Button EndTurnButton; void Awake() { if (Instance null) { Instance this; } } void Start() { // 订阅游戏管理器的事件 GameManager.Instance.OnTurnBegin UpdateUI; GameManager.Instance.OnTurnEnd UpdateUI; // 按钮事件 EndTurnButton.onClick.AddListener(GameManager.Instance.UI_EndTurnButton); // 初始更新 UpdateUI(); } public void UpdateUI() { UpdateGameStateUI(); // 可以在这里更新其他UI如选中单位的面板 } void UpdateGameStateUI() { if (GameManager.Instance null) return; Player currentPlayer GameManager.Instance.Players[GameManager.Instance.CurrentPlayerIndex]; TurnCounterText.text $Turn: {GameManager.Instance.CurrentPlayerIndex 1}; // 简化回合数 CurrentPlayerText.text $Player: {currentPlayer.Name}; CurrentPlayerText.color currentPlayer.Color; // 更新资源显示这里显示当前玩家的总资源 ResourceFoodText.text $Food: {currentPlayer.TotalResources.Food}; ResourceProductionText.text $Prod: {currentPlayer.TotalResources.Production}; ResourceGoldText.text $Gold: {currentPlayer.TotalResources.Gold}; ResourceScienceText.text $Sci: {currentPlayer.TotalResources.Science}; } // 当选中一个单位或城市时调用 public void ShowSelectionInfo(string name, string details) { SelectionPanel.SetActive(true); SelectionNameText.text name; SelectionDetailsText.text details; } public void HideSelectionInfo() { SelectionPanel.SetActive(false); } }4.2 城市管理UI当玩家点击自己的城市时应弹出一个管理界面允许查看详情、选择建造项目等。// 文件路径Assets/Scripts/UI/CityUI.cs using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; public class CityUI : MonoBehaviour { public GameObject CityPanel; public TMP_Text CityNameText; public TMP_Text CityYieldText; public Transform BuildingListParent; public GameObject BuildingButtonPrefab; private City currentCity; void Start() { CityPanel.SetActive(false); } public void OpenCityPanel(City city) { currentCity city; CityPanel.SetActive(true); UpdateCityInfo(); PopulateBuildingList(); } public void CloseCityPanel() { CityPanel.SetActive(false); currentCity null; } void UpdateCityInfo() { if (currentCity null) return; CityNameText.text currentCity.CityName; CityYieldText.text $Food: {currentCity.PerTurnYield.Food} | Prod: {currentCity.PerTurnYield.Production} | Gold: {currentCity.PerTurnYield.Gold} | Sci: {currentCity.PerTurnYield.Science}; } void PopulateBuildingList() { // 清除现有按钮 foreach (Transform child in BuildingListParent) { Destroy(child.gameObject); } // 加载所有可建造的建筑通常从Resources文件夹或配置表 Building[] allBuildings Resources.LoadAllBuilding(Buildings); foreach (var building in allBuildings) { GameObject buttonObj Instantiate(BuildingButtonPrefab, BuildingListParent); TMP_Text buttonText buttonObj.GetComponentInChildrenTMP_Text(); buttonText.text ${building.BuildingName} ({building.ProductionCost}); Button button buttonObj.GetComponentButton(); button.onClick.AddListener(() OnBuildingSelected(building)); } } void OnBuildingSelected(Building building) { if (currentCity ! null) { currentCity.SetProduction(building); UpdateCityInfo(); // 更新显示可能显示“建造中” } } }然后在InputController的点击处理中加入打开城市UI的逻辑。5. 性能优化与高级技巧随着星球变大、单位增多性能会成为瓶颈。以下是一些针对此项目的关键优化点。5.1 网格数据与寻路优化使用数组代替GameObject查找HexGrid中的单元格引用应存储在HexCell[,]或ListHexCell中避免频繁使用GameObject.Find或GetComponent。对象池对于单位、特效等频繁创建销毁的对象使用对象池。寻路算法优化使用高效的A*算法并利用六边形网格的特性每个单元格只有6个邻居来加速。可以为移动成本设计启发式函数。// 简化的A*寻路示例需配合优先队列 public ListHexCell FindPath(HexCell startCell, HexCell endCell) { // 这是一个复杂主题需要实现开放列表、关闭列表和代价计算。 // 推荐使用现有的库如 A* Pathfinding Project或仔细实现一个。 // 核心是 F G H其中G是移动成本H是到终点的估算成本如曼哈顿距离。 return null; // 返回路径列表 }5.2 渲染优化合批Batching确保使用相同材质的六边形单元格能够进行静态或动态合批。减少Draw Call。LODLevel of Detail对于远离相机的地形块使用面数更少的模型或甚至用公告板Billboard代替。视锥体剔除Frustum CullingUnity默认开启确保你的地形块Chunk大小设置合理。战争迷雾与视野计算不要每帧更新所有单元格的可见性。只在单位移动或回合结束时基于单位位置重新计算。5.3 序列化与保存游戏为了让玩家可以保存进度需要序列化游戏状态。// 文件路径Assets/Scripts/SaveLoad/GameSaveData.cs using System; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class GameSaveData { public int CurrentTurn; public int CurrentPlayerIndex; public ListPlayerSaveData Players new ListPlayerSaveData(); public ListHexCellSaveData HexCells new ListHexCellSaveData(); // ... 保存单位、城市等数据 } [System.Serializable] public class PlayerSaveData { public int Id; public string Name; public float[] ColorRGB; // 保存Color的RGB值 public ResourceStack TotalResources; } [System.Serializable] public class HexCellSaveData { public int X; public int Z; public float Elevation; public float Moisture; public int TerrainTypeIndex; public int OwnerId; // ... 其他需要保存的属性 } public class SaveLoadManager : MonoBehaviour { public void SaveGame(string saveFileName) { GameSaveData saveData new GameSaveData(); GameManager gm GameManager.Instance; saveData.CurrentTurn gm.CurrentPlayerIndex; // 简化 saveData.CurrentPlayerIndex gm.CurrentPlayerIndex; // 保存玩家数据 foreach (var player in gm.Players) { PlayerSaveData psd new PlayerSaveData(); psd.Id player.Id; psd.Name player.Name; psd.ColorRGB new float[] { player.Color.r, player.Color.g, player.Color.b }; psd.TotalResources player.TotalResources; saveData.Players.Add(psd); } // 保存网格数据 HexGrid grid FindObjectOfTypeHexGrid(); foreach (var cell in grid.Cells) { HexCellSaveData hcsd new HexCellSaveData(); hcsd.X cell.coordinates.X; hcsd.Z cell.coordinates.Z; hcsd.Elevation cell.Elevation; hcsd.Moisture cell.Moisture; hcsd.TerrainTypeIndex (int)cell.TerrainType; hcsd.OwnerId cell.OwnerId; saveData.HexCells.Add(hcsd); } // 转换为JSON并保存到文件 string json JsonUtility.ToJson(saveData, true); System.IO.File.WriteAllText(Application.persistentDataPath / saveFileName .json, json); Debug.Log(Game saved to: Application.persistentDataPath / saveFileName .json); } public void LoadGame(string saveFileName) { string filePath Application.persistentDataPath / saveFileName .json; if (System.IO.File.Exists(filePath)) { string json System.IO.File.ReadAllText(filePath); GameSaveData saveData JsonUtility.FromJsonGameSaveData(json); // 根据saveData重建游戏状态 // 1. 清空当前场景 // 2. 重新生成网格使用保存的Elevation, Moisture等 // 3. 实例化玩家、单位、城市并设置其属性 // 这是一个复杂的流程需要仔细设计 Debug.Log(Game loaded.); } else { Debug.LogError(Save file not found: filePath); } } }6. 常见问题与排查思路在开发过程中你可能会遇到以下典型问题问题现象常见原因解决思路单位移动时卡顿或掉帧1. 寻路算法效率低如未使用A*。2. 每帧更新所有单元格的高亮显示。3. 物理射线检测过多。1. 实现或换用高效的A*算法并限制寻路深度。2. 只在选择单位时计算并高亮一次移动范围而不是每帧更新。3. 使用Physics.RaycastNonAlloc或减少射线检测频率。生成大型星球时内存溢出或崩溃1. 一次性实例化所有单元格的GameObject。2. 每个单元格的Mesh或Collider过于复杂。3. 未使用分块Chunk加载。1. 实现分块系统只加载相机附近的区块。2. 简化单元格的网格使用LOD。3. 使用对象池管理单元格。战争迷雾FOV计算缓慢每帧为所有单位重新计算整个地图的可见性。1. 使用更高效的视野算法如阴影投射或预计算的可见性掩码。2. 只在单位移动或回合结束时计算。3. 将可见性计算分摊到多帧完成。UI文本不更新1. 未订阅GameManager的事件。2.UpdateUI方法未被调用。3. TextMeshPro组件未正确引用。1. 在UIManager.Start()中确认事件订阅成功。2. 确保在资源变化、回合结束时手动调用UIManager.Instance.UpdateUI()。3. 检查Inspector面板中的引用是否丢失。保存/加载后游戏状态错乱1. 序列化数据不完整漏掉了关键字段。2. 加载顺序错误对象依赖关系未重建。3. 使用了Unity无法直接序列化的类型如Dictionary。1. 检查GameSaveData类确保所有必要数据都被包含。2. 设计清晰的加载流程先重建网格再实例化玩家最后放置单位和建筑。3. 将Dictionary转换为List进行存储。不同地形边缘有缝隙1. 六边形网格的顶点位置计算有浮点误差。2. 不同地形使用了不同的材质球渲染顺序问题。1. 确保顶点坐标计算使用一致的精度或在生成网格后调用mesh.RecalculateNormals()。2. 使用共享材质或调整渲染队列Render Queue。7. 最佳实践与工程建议使用ScriptableObject管理静态数据如单位属性、建筑属性、科技树。这使平衡调整和本地化变得非常方便无需修改代码。实现事件系统避免GameManager、UIManager、Unit、City之间紧密耦合。使用C#的event或UnityEvent进行通信。例如OnResourceChanged、OnUnitMoved。分离数据与表现HexCell应主要存储数据其外观更新通过一个独立的HexCellView组件来处理。这符合MVC/MVP模式便于维护和扩展。为性能关键代码使用Job System和Burst Compiler如果使用Unity较新版本对于大规模的地形生成、视野计算、寻路等可以考虑使用C# Job System和Burst编译器来利用多核CPU获得巨大的性能提升。编写自定义编辑器工具在Unity Editor中创建自定义Inspector和窗口用于快速生成地图、调整噪声参数、批量设置单元格属性能极大提升开发效率。版本控制与场景管理将预制体、脚本、ScriptableObject等资源纳入版本控制如Git。避免在场景中保存大量动态生成的游戏对象而是通过代码在运行时实例化。进行单元测试为核心算法如六边形坐标转换、寻路、收益计算编写单元测试确保其正确性尤其是在进行性能优化或重构时。至此我们已经将一个静态的六边形星球转变为一个拥有完整经济循环、单位移动、玩家交互和UI管理的策略游戏原型。从噪声生成到游戏逻辑从底层网格到顶层交互你已经掌握了构建此类游戏的核心模块。当然一个完整的游戏还需要音效、更复杂的AI、科技树、外交系统等但本系列提供的框架是一个坚实可靠的起点。
返回列表