Unity RTS/TD游戏:从网格数据到动态建造的实战解析

张开发
2026/4/20 22:51:20 15 分钟阅读

分享文章

Unity RTS/TD游戏:从网格数据到动态建造的实战解析
1. 网格系统RTS/TD游戏的建造基石在RTS即时战略和TD塔防游戏中网格系统就像现实世界中的建筑工地测量仪。想象一下你要在一片凹凸不平的荒地上建造城堡首先得用石灰粉画出整齐的方格线标记出哪些区域适合打地基。Unity中的网格系统就是数字世界的石灰线它把复杂的三维地形转化为规整的二维坐标系。我做过一个中世纪题材的TD项目当时用这个结构体存储每个格子信息[System.Serializable] public struct GridCell { public Vector2Int coordinate; // 网格坐标(X,Z) public float height; // 地形高度 public float slopeAngle; // 坡度角度 public bool isOccupied; // 是否被建筑占据 public Building building; // 关联的建筑对象 }初始化网格时有个坑要注意地形尺寸与网格密度的平衡。在《帝国时代》这类游戏中1个单位通常代表1米如果单元格设为2x2米会显得建筑摆放太稀疏0.5x0.5米又会导致性能问题。我的经验公式是小型建筑箭塔等占2x2格子中型建筑兵营等占3x3格子大型建筑主城等占4x4格子实测发现将TerrainData的heightmap分辨率设为129x129配合2米单元格大小既能保证建造精度又不会过度消耗内存。初始化代码关键部分如下void GenerateGrid() { TerrainData terrainData terrain.terrainData; gridSize new Vector2Int( Mathf.CeilToInt(terrainData.size.x / cellSize), Mathf.CeilToInt(terrainData.size.z / cellSize) ); gridCells new GridCell[gridSize.x, gridSize.y]; for (int x 0; x gridSize.x; x) { for (int y 0; y gridSize.y; y) { Vector3 worldPos new Vector3( x * cellSize cellSize * 0.5f, 0, y * cellSize cellSize * 0.5f ); // 获取地形高度和坡度 Vector2 normalizedPos new Vector2( worldPos.x / terrainData.size.x, worldPos.z / terrainData.size.z ); gridCells[x, y].height terrainData.GetInterpolatedHeight(normalizedPos.x, normalizedPos.y); gridCells[x, y].slopeAngle terrainData.GetSteepness(normalizedPos.x, normalizedPos.y); } } }2. 动态建造的三大核心交互2.1 鼠标悬停检测像磁铁吸附一样自然在《星际争霸2》中建筑会像被磁铁吸住一样自动对齐到网格这背后是射线检测与网格坐标转换的魔法。我推荐采用分层检测策略第一层地形碰撞检测Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f, terrainLayerMask)) { Vector2Int gridPos WorldToGridPosition(hit.point); }第二层UI遮挡判断if (EventSystem.current.IsPointerOverGameObject()) { return; // 鼠标在UI上时不处理建造 }第三层建筑碰撞检测Collider[] colliders Physics.OverlapBox(hit.point, buildingSize, Quaternion.identity, buildingLayerMask); bool canPlace colliders.Length 0;提示WorldToGridPosition方法需要处理地形高度差建议用Mathf.RoundToInt代替直接强制转换避免浮点误差导致的坐标偏移。2.2 放置预览给玩家明确的视觉反馈好的预览系统应该像汽车倒车雷达用颜色和形状告诉玩家这里能不能停。我常用的方案是可放置状态半透明绿色模型网格线不可放置状态红色闪烁模型阻挡区域高亮特殊状态黄色表示需要满足前置条件如附近要有主城Shader代码实现轮廓光效果// 在片段着色器中添加 float rim 1 - saturate(dot(normalize(i.worldNormal), normalize(_WorldSpaceCameraPos - i.worldPos))); float3 rimColor _CanBuild ? float3(0,1,0) : float3(1,0,0); o.Emission rimColor * pow(rim, _RimPower) * _RimIntensity;2.3 即时验证建造条件的多重校验在《魔兽争霸3》中即使金钱足够在斜坡上造建筑也会被拒绝。我们需要实现类似的智能验证public bool ValidateBuildingPlacement(Vector2Int gridPos, BuildingType type) { BuildingData data buildingDataDict[type]; // 检查是否超出地图边界 if (gridPos.x 0 || gridPos.x data.Width gridSize.x || gridPos.y 0 || gridPos.y data.Height gridSize.y) { return false; } // 检查地形坡度 float maxSlope 0; for (int x gridPos.x; x gridPos.x data.Width; x) { for (int y gridPos.y; y gridPos.y data.Height; y) { maxSlope Mathf.Max(maxSlope, gridCells[x, y].slopeAngle); if (gridCells[x, y].isOccupied) return false; } } return maxSlope data.MaxAllowedSlope; }3. 高级建造效果实现技巧3.1 动态网格绘制Shader魔法传统Gizmos绘制在移动端性能堪忧我用ProjectorShader方案实现了《文明6》风格的动态网格// 网格线Shader核心算法 float2 uv i.uv * _GridSize; float2 grid abs(frac(uv - 0.5) - 0.5) / fwidth(uv); float line min(grid.x, grid.y); float4 color saturate(1 - line) * _GridColor; // 添加抗锯齿 float2 derivative fwidth(uv); float2 pixelUV uv * _MainTex_TexelSize.zw; float pixelSize length(derivative) * 0.707; // 对角线系数 color.a smoothstep(0.5 - pixelSize, 0.5 pixelSize, color.a);注意Projector要设置Orthographic正交投影Near/Far Clip Plane根据地形高度差调整避免穿帮。3.2 建造动画从零到完整的生长过程没有美术资源用Shader动画照样能做出惊艳效果。这是我自研的建筑生长Shader控制参数_Progress (0-1)控制建造进度_DissolveHeight溶解边缘高度_GlowIntensity建造时的发光强度// 在建造协程中动态调整参数 IEnumerator BuildingProgress(Material mat, float duration) { float timer 0; while (timer duration) { timer Time.deltaTime; float progress timer / duration; mat.SetFloat(_Progress, progress); mat.SetFloat(_GlowIntensity, Mathf.PingPong(progress * 2, 1)); yield return null; } }3.3 多单位格处理非对称建筑对齐当建筑占用偶数个格子时如2x4直接取中心点会导致视觉偏差。解决方案是引入对齐偏移量public Vector3 GetSnappedPosition(Vector3 rawPos, Vector2Int buildingSize) { Vector2Int gridPos WorldToGridPosition(rawPos); Vector3 center GridToWorldPosition(gridPos); // 处理偶数尺寸偏移 if (buildingSize.x % 2 0) center.x - cellSize * 0.5f; if (buildingSize.y % 2 0) center.z - cellSize * 0.5f; // 保持y轴与地形贴合 center.y GetTerrainHeightAt(center); return center; }4. 性能优化实战经验4.1 网格数据查询优化在500x500的网格中频繁调用GetCell()会导致CPU瓶颈。我采用分层存储策略基础层原始高度图数据缓存层最近访问的区块类似CPU缓存标记层用BitArray存储占用状态private DictionaryVector2Int, GridChunk cachedChunks new DictionaryVector2Int, GridChunk(); public GridCell GetCell(Vector2Int gridPos) { Vector2Int chunkPos new Vector2Int( gridPos.x / CHUNK_SIZE, gridPos.y / CHUNK_SIZE ); if (!cachedChunks.TryGetValue(chunkPos, out GridChunk chunk)) { chunk LoadChunkFromDisk(chunkPos); cachedChunks[chunkPos] chunk; if (cachedChunks.Count MAX_CACHED_CHUNKS) { // LRU缓存淘汰 } } return chunk.GetLocalCell(gridPos.x % CHUNK_SIZE, gridPos.y % CHUNK_SIZE); }4.2 批量绘制优化用Graphics.DrawMeshInstanced批量绘制网格指示器比单独GameObject性能提升10倍MaterialPropertyBlock props new MaterialPropertyBlock(); Matrix4x4[] matrices new Matrix4x4[validCells.Count]; Color[] colors new Color[validCells.Count]; for (int i 0; i validCells.Count; i) { matrices[i] Matrix4x4.TRS( GridToWorldPosition(validCells[i]), Quaternion.identity, Vector3.one * cellSize ); colors[i] GetCellColor(validCells[i]); } props.SetVectorArray(_Color, colors); Graphics.DrawMeshInstanced(gridMesh, 0, gridMaterial, matrices, matrices.Length, props);4.3 异步加载策略大型地图采用分帧加载避免卡顿IEnumerator LoadGridDataAsync() { int cellsPerFrame Mathf.Max(gridSize.x * gridSize.y / 10, 100); int processed 0; for (int x 0; x gridSize.x; x) { for (int y 0; y gridSize.y; y) { gridCells[x, y] CalculateCellData(x, y); processed; if (processed % cellsPerFrame 0) { yield return null; // 每处理一定数量单元格就暂停一帧 } } } }在最近的一个塔防项目中这些优化使建造系统的CPU耗时从8ms降到了1.2ms内存占用减少了65%。关键是要根据实际游戏规模选择合适的技术方案——小型地图可以用全内存存储大型开放世界则需要更智能的流式加载。

更多文章