Unity RTS/TD游戏:从网格数据到动态建造的实战架构
1. 网格数据容器的设计与初始化在RTS/TD游戏中网格系统是整个建造机制的基础骨架。想象一下就像在现实世界中建造房屋需要先划分地块一样游戏中的建造系统也需要一个精确的坐标参考系。这里我们采用二维数组MapCellNode[,]作为核心数据结构每个单元格记录着三个关键信息地形高度用于判断建造物是否能够平稳放置坡度数据防止在悬崖等陡峭地形违规建造建筑引用记录当前格子是否已被占用初始化网格时有个实用技巧我习惯将地形数据的bounds.size与预设的cellSize相除得到网格行列数这样能确保网格完美覆盖整个地图。实测发现cellSize设置为2x2米是个不错的起点既能保证建造精度又不会让计算量爆炸。private void InitMapCells() { var terrainData _terrain.terrainData; int gridWidth (int)(terrainData.bounds.size.x / cellSize.x); int gridHeight (int)(terrainData.bounds.size.z / cellSize.y); mapCells new MapCellNode[gridWidth, gridHeight]; for (int i 0; i gridWidth; i) { for (int j 0; j gridHeight; j) { var center GetCellLocalPosition(i, j); mapCells[i, j] new MapCellNode { height center.y, steepness terrainData.GetSteepness( center.x / terrainData.size.x, center.z / terrainData.size.z), current null }; } } }踩过的一个坑直接使用世界坐标计算网格索引会导致边缘误差。后来改用transform.InverseTransformPoint将世界坐标转换到本地空间后再计算精度问题迎刃而解。这个细节处理不好建筑边缘会出现诡异的悬空或陷地现象。2. 建造状态机的完整生命周期2.1 PreBuilding状态管理当玩家点击建造按钮时系统会进入PreBuilding状态。这个阶段就像现实中的建筑工地放样需要处理三个核心问题视觉反馈系统用不同颜色材质绿色可建造/红色不可建造实时反馈放置合法性地形适配建筑底部自动贴合地形曲面避免浮空楼阁碰撞检测通过射线检测和网格查询实现双重校验public bool CheckBuildingIndexPosOnGrid(int x, int y) { bool canBuild true; var (w, h) GetRealSizeWithDir(); // 考虑旋转后的实际占用尺寸 // 计算建筑底座覆盖的所有网格 for (int px x - w/2; px x w/2; px) { for (int py y - h/2; py y h/2; py) { if (!IsCellBuildable(px, py)) { canBuild false; break; } } } // 更新预览材质 preMaterial.SetColor(_Color, canBuild ? BuildManager.preBuildNormalColor : BuildManager.preBuildBadColor); return canBuild; }2.2 建造确认与实例化当玩家左键确认位置后系统会经历三个关键步骤数据校验二次确认所有占用网格的合法性动画触发播放建造动画我用Shader实现了个简易的从下往上生长效果数据固化将建筑实例写入网格数据容器这里有个性能优化点不要在Update里直接实例化预制体而是采用对象池预加载。实测在手机端对象池能使建造卡顿减少70%以上。3. 地形适配的进阶处理3.1 动态高度匹配建筑放置时需要智能适应地形起伏。我的方案是计算目标区域的平均高度然后用这个值作为建筑基准面。但要注意两个特殊情况悬崖边缘当区域高度差超过阈值建议0.2米时禁止建造斜坡建造通过判断steepness值决定是否允许放置public static float GetGridAverageHeight(int sx, int sy, int w, int h) { float totalHeight 0; int validCells 0; for (int x sx; x sx w; x) { for (int y sy; y sy h; y) { if (IsValidCell(x, y)) { totalHeight mapCells[x, y].height; validCells; } } } return validCells 0 ? totalHeight / validCells : 0; }3.2 网格可视化方案为了让玩家直观看到建造范围我开发了基于Projector的网格着色器。关键点在于使用frac函数生成重复网格图案通过fwidth实现抗锯齿添加距离衰减系数使边缘渐变消失这个方案比Gizmos绘制性能更好在低端设备上也能保持60FPS。记得要把Projector的材质设为Transparent并适当调整Far Clip距离。4. 防御塔的特殊处理逻辑塔防游戏中的防御塔通常需要额外处理两个特性攻击范围指示器用环形投影显示作用半径朝向系统通过快捷键旋转调整攻击方向// 防御塔旋转控制 if (Input.GetKeyDown(KeyCode.R)) { currentBuilding.NextRotation(); // 更新攻击范围指示器位置 attackIndicator.transform.localPosition currentBuilding.GetAttackOriginOffset(); }对于范围指示器我写了个特别的Shader内圈透明外圈半透明中间用硬边过渡。通过step和smoothstep的组合使用既能清晰显示边界又不会显得生硬。5. 性能优化实战经验在大规模地图上建造系统容易成为性能瓶颈。经过多次测试我总结出这些优化手段分帧处理将网格校验拆解到多帧完成空间分区用四叉树管理活跃建造区域LOD控制远离摄像头的建筑使用简版碰撞体JobSystem并行将地形高度计算交给Burst编译的Job特别提醒在移动端要避免每帧更新所有预览物件的材质属性。可以通过Shader的_Time变量实现动画效果减少CPU-GPU通信开销。6. 调试与异常处理开发过程中最头疼的就是建筑莫名消失的问题。后来我建立了完整的调试视图网格Gizmos绘制在Scene视图显示网格边界建筑占用标记用不同颜色标注已占用/空闲格子地形参数可视化用颜色渐变表示高度和坡度#if UNITY_EDITOR void OnDrawGizmosSelected() { if (!showDebug) return; for (int x 0; x gridSize.x; x) { for (int y 0; y gridSize.y; y) { var pos GetCellWorldPosition(x, y); Gizmos.color mapCells[x,y].current ! null ? Color.red : Color.green; Gizmos.DrawWireCube(pos, new Vector3(cellSize.x, 0.1f, cellSize.y)); } } } #endif遇到建筑无法放置时建议按这个顺序排查检查网格索引计算是否正确验证地形高度差是否在允许范围内确认碰撞体层级设置查看是否有其他建筑引用未被释放7. 扩展设计思路这套架构可以轻松扩展更多建造玩法多层级建造在Z轴上添加楼层管理组合建筑将多个小建筑组合成超级建筑地形改造允许玩家平整土地或开挖隧道动态网格实现可破坏的地形系统最近我在一个项目中实现了动态网格调整当玩家使用炸弹时爆炸范围内的网格会被标记为不可建造状态直到被工程师修复。这个功能只需要在现有系统上扩展一个GridState枚举即可。