Unity Tilemap实战:用Sprite+Shader搞定SLG大地图无缝地表(附完整Shader代码)
Unity Tilemap实战Sprite与Shader打造SLG大地图无缝地表在策略游戏(SLG)开发中大地图的表现直接影响着玩家的沉浸感和游戏体验。传统网格拼贴方案虽然直观但在处理无缝地表时往往面临美术资源制作复杂、性能开销大等问题。本文将深入探讨一种更优雅的解决方案——Sprite结合自定义Shader实现无缝地表渲染。1. 为什么选择SpriteShader方案SLG大地图通常需要呈现广阔的游戏世界地表纹理的重复使用是不可避免的。传统Tilemap方案中每个网格单元都需要独立的纹理资源这不仅增加了美术工作量还可能导致以下问题接缝明显相邻网格边缘容易出现视觉断裂内存占用高大量重复但独立的纹理资源Draw Call激增每个网格单元都需要单独渲染相比之下SpriteShader方案具有显著优势性能对比表指标传统TilemapSpriteShader纹理内存高(多份重复)低(单份共享)Draw Call随网格数增加固定少量美术工作量高(需分割)低(完整图)无缝效果需额外处理原生支持提示在移动端SLG项目中Draw Call优化尤为关键SpriteShader方案通常能减少50%以上的地表渲染开销。2. 核心实现原理2.1 Sprite的WrapMode设置Unity中的SpriteRenderer默认使用Clamp模式超出UV范围的纹理会被截断。要实现无缝平铺首先需要修改纹理的WrapModeTexture2D mainTex Resources.LoadTexture2D(GroundTexture); mainTex.wrapMode TextureWrapMode.Repeat;这一步确保纹理能够在UV坐标超出[0,1]范围时自动重复而不是拉伸边缘像素。2.2 自定义Shader编写基础的无缝滚动Shader需要处理两个核心功能UV坐标的无限重复根据摄像机位置自动调整纹理偏移Shader Custom/ScrollingGround { Properties { _MainTex (Base (RGB), 2D) white {} _ScrollSpeed (Scroll Speed, Vector) (0.1, 0.1, 0, 0) } SubShader { Tags { RenderTypeOpaque } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float2 _ScrollSpeed; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); // 计算世界空间下的UV偏移 float2 worldUV mul(unity_ObjectToWorld, v.vertex).xz; o.uv TRANSFORM_TEX(worldUV, _MainTex) _ScrollSpeed * _Time.y; return o; } fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); } ENDCG } } }2.3 动态滚动算法优化为了实现地表纹理随摄像机移动而自然滚动需要将摄像机位置纳入偏移计算// C#脚本中更新Shader参数 Material groundMat GetComponentRenderer().material; Vector2 scrollOffset new Vector2( cameraTransform.position.x / textureSize, cameraTransform.position.z / textureSize ); groundMat.SetVector(_ScrollOffset, scrollOffset);对应的Shader修改// 在vert函数中添加 o.uv _ScrollOffset;3. 高级效果实现3.1 多纹理混合单一地表纹理往往显得单调可以通过Shader混合多张纹理增加细节Properties { _MainTex (Base Texture, 2D) white {} _DetailTex (Detail Texture, 2D) white {} _BlendFactor (Blend Factor, Range(0,1)) 0.5 } // 在frag函数中 fixed4 base tex2D(_MainTex, i.uv); fixed4 detail tex2D(_DetailTex, i.uv * 5.0); // 更高频的UV return lerp(base, detail, _BlendFactor);3.2 动态地貌效果通过顶点着色器添加简单的波动效果可以模拟水面或特殊地形v2f vert (appdata v) { v2f o; // 添加基于时间的正弦波动 v.vertex.y sin(_Time.y v.vertex.x * 0.1) * 0.1; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; }3.3 性能优化技巧纹理压缩使用ASTC或ETC2压缩格式合批处理确保所有使用相同Shader的地表对象满足静态合批条件LOD分级根据摄像机距离调整Shader复杂度// LOD示例代码 void Update() { float dist Vector3.Distance(transform.position, camera.position); if(dist 50f) { material.SetFloat(_DetailLevel, 0.5f); } else { material.SetFloat(_DetailLevel, 1.0f); } }4. 实战案例SLG雪原地表实现以下是一个完整的雪原地表实现方案包含动态脚印效果美术资源准备基础雪地纹理2048x2048脚印法线贴图512x512雪地细节噪声图1024x1024Shader核心代码Shader Custom/SnowField { Properties { _MainTex (Snow Texture, 2D) white {} _DetailNoise (Detail Noise, 2D) gray {} _FootprintTex (Footprint, 2D) black {} _FootprintPositions (Footprints, VectorArray) {} _FootprintCount (Footprint Count, Int) 0 } SubShader { // ... 省略常规设置 void surf (Input IN, inout SurfaceOutputStandard o) { // 基础雪地 fixed4 snow tex2D(_MainTex, IN.worldPos.xz * 0.01); // 添加细节噪声 fixed noise tex2D(_DetailNoise, IN.worldPos.xz * 0.1).r; snow.rgb * 1.0 - noise * 0.2; // 混合脚印 for(int i 0; i _FootprintCount; i) { float2 delta IN.worldPos.xz - _FootprintPositions[i].xy; float dist length(delta); if(dist _FootprintPositions[i].z) { float2 fpUV delta / _FootprintPositions[i].z * 0.5 0.5; fixed4 fp tex2D(_FootprintTex, fpUV); snow.rgb lerp(snow.rgb, fp.rgb, fp.a); } } o.Albedo snow.rgb; o.Smoothness snow.a; } } }C#控制脚本public class SnowFieldController : MonoBehaviour { private Material snowMat; private ListVector4 footprints new ListVector4(); void Start() { snowMat GetComponentRenderer().material; } public void AddFootprint(Vector3 position, float size) { footprints.Add(new Vector4(position.x, position.z, size, 0)); if(footprints.Count 20) footprints.RemoveAt(0); snowMat.SetInt(_FootprintCount, footprints.Count); snowMat.SetVectorArray(_FootprintPositions, footprints); } void Update() { // 随时间淡化脚印 for(int i 0; i footprints.Count; i) { footprints[i] new Vector4( footprints[i].x, footprints[i].y, footprints[i].z, footprints[i].w Time.deltaTime * 0.1f ); } } }在实际项目中这种方案相比传统Tilemap减少了约70%的纹理内存占用Draw Call从原来的数百次降低到个位数同时提供了更丰富的视觉效果。