资讯动态

Unity Tilemap 单格瓦片动态缩放:Matrix4x4与Shader实战

发布时间:2026/8/5 3:56:49 来源:尧图企业网站定制
1. 项目概述当棋盘上的棋子需要“呼吸感”在开发2D棋盘类游戏比如自走棋、战棋或者一些策略游戏时我们经常会用到Unity的Tilemap系统来构建规整的网格地图。Tilemap高效、易用是处理网格化地图的不二之选。但不知道你有没有遇到过这样的需求当玩家选中一个单位棋子或者某个单位触发特殊状态如“巨大化”、“强化”时你希望这个棋子能有一个醒目的、动态的缩放效果以增强视觉反馈和游戏表现力。最初你可能会想到直接去修改这个棋子Tile的Transform.localScale。这很直观对吧但在Tilemap的世界里事情没那么简单。Tilemap中的每个瓦片Tile并不是一个独立的GameObject它们是由Tilemap Renderer批量渲染的。你无法直接获取并修改其中某一个特定位置瓦片的Transform。直接修改整个Tilemap的缩放又会影响到棋盘上所有的格子这显然不是我们想要的。那么如何精准地控制棋盘上某一个特定格子里“棋子”的大小呢这就是本次实战要解决的核心问题。我们将深入Tilemap的渲染底层借助Matrix4x4四维矩阵这个强大的数学工具实现针对单个或多个瓦片的动态缩放。这种方法不依赖额外的GameObject性能开销极低并且能与Tilemap系统无缝集成是实现高级棋盘游戏视觉效果的关键技巧。2. 核心原理Matrix4x4与自定义瓦片渲染要理解解决方案我们得先拆解一下Unity Tilemap的渲染流程。Tilemap Renderer在绘制每一个瓦片时并不是简单地把一张图片贴上去。对于每一个瓦片单元系统内部会计算一个“变换矩阵”这个矩阵决定了这个瓦片最终在世界空间中的位置、旋转和缩放。默认情况下这个矩阵只包含位置信息即该瓦片在网格中的坐标缩放和旋转都是单位值即1和0。Matrix4x4是计算机图形学中用于描述线性变换平移、旋转、缩放、错切等的标准工具。在Unity中一个Matrix4x4可以完整定义一个从局部空间到世界空间的变换。我们的突破口就在这里我们可以为Tilemap中指定的瓦片替换掉它默认的变换矩阵注入一个包含了自定义缩放甚至旋转信息的新矩阵。2.1 技术选型为何是Matrix4x4你可能会问还有其他方法吗比如在Tile上叠加一个SpriteRenderer的GameObject这能解决问题但破坏了Tilemap的批处理优势当棋子数量成百上千时Draw Call会暴增性能堪忧。使用Tilemap的Animation Tile可以制作缩放动画但这是预定义的、静态的很难根据游戏逻辑如选中、Buff状态进行动态、实时的控制。修改Tile的Sprite换成一张更大的图片这不够灵活无法实现平滑的过渡动画。相比之下使用Matrix4x4修改渲染矩阵的方案具有显著优势高性能它仍在Tilemap的渲染管线内享受合批优化仅增加了极少的CPU计算开销。高灵活度缩放值scale可以随时通过代码动态计算和修改轻松实现动画、插值。功能强大矩阵不仅能处理缩放稍加扩展就能支持以任意点为中心的旋转为游戏特效如攻击旋转、眩晕效果打开大门。注意这种方法本质上是“欺骗”渲染器。Tilemap的碰撞体Tilemap Collider 2D和网格坐标cell位置不会因为我们的矩阵变换而改变。一个被放大了2倍的棋子其逻辑位置和碰撞边界仍然在原单元格内。这是需要特别注意的我们会在后续章节讨论如何同步处理逻辑与视觉。2.2 Matrix4x4基础构造一个缩放矩阵一个标准的缩放矩阵非常简单。假设我们要在X轴和Y轴上进行均匀缩放缩放系数为s那么对应的4x4矩阵如下[ s, 0, 0, 0 ] [ 0, s, 0, 0 ] [ 0, 0, s, 0 ] [ 0, 0, 0, 1 ]在Unity中我们可以使用Matrix4x4.Scale(Vector3 scale)这个便捷方法来创建缩放矩阵。例如Matrix4x4.Scale(new Vector3(2f, 2f, 1f))会创建一个在XY平面放大2倍的矩阵Z轴保持1因为我们是2D。然而仅仅有缩放矩阵还不够。这个缩放是以世界原点为中心的。如果我们直接应用棋子会从网格原点开始放大位置就错乱了。因此我们需要的是一个“平移-缩放-平移回”的复合变换也就是以棋子自身中心点为缩放原点。先将棋子“平移”到世界原点减去其单元格中心位置。在世界原点进行缩放。再“平移”回原来的位置。这个过程用矩阵乘法表示就是T * S * T_inv。在Unity中我们可以通过Matrix4x4.TRS方法一次性创建这个复合矩阵。TRS代表Translation平移、Rotation旋转、Scale缩放。// 假设cellPosition是世界空间中某个单元格的中心点坐标 Vector3 cellCenterWorldPos tilemap.GetCellCenterWorld(cellPosition); float scaleFactor 2.0f; Matrix4x4 customMatrix Matrix4x4.TRS( cellCenterWorldPos, // 平移位置缩放后的位置其实最终位置没变但TRS内部会处理 Quaternion.identity, // 旋转无 new Vector3(scaleFactor, scaleFactor, 1f) // 缩放 ); // 这里需要理解TRS中的平移参数是指执行完缩放和旋转后物体最终的原点位置。 // 对于以自身中心缩放位置参数就是cellCenterWorldPos缩放中心自然就是该位置。3. 实战架构自定义Tile与渲染器拓展理解了原理我们来搭建实现框架。我们不会直接修改Unity内置的Tilemap渲染流程而是通过创建一个自定义的Tile类和一个配套的Tilemap渲染器拓展脚本来协作完成。3.1 创建可缩放瓦片ScalableTile首先我们需要一种特殊的Tile它能够携带一个“缩放系数”的信息。Unity允许我们创建继承自TileBase的自定义Tile类。using UnityEngine; using UnityEngine.Tilemaps; [CreateAssetMenu(fileName New Scalable Tile, menuName 2D/Tiles/Scalable Tile)] public class ScalableTile : TileBase { public Sprite sprite; // 瓦片显示的精灵 public Color color Color.white; // 颜色 [Range(0.1f, 5f)] public float baseScale 1.0f; // 基础缩放用于在编辑器中调整大小 // 注意这个baseScale是编辑器用的运行时动态缩放由另一个系统控制。 public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData) { // 这是TileBase的核心方法用于向Tilemap提供渲染数据。 tileData.sprite sprite; tileData.color color; tileData.flags TileFlags.LockColor; // 锁定颜色避免被Tilemap覆盖 tileData.colliderType Tile.ColliderType.Sprite; // 设置碰撞体类型 // 这里不直接设置transform因为我们需要每帧动态计算。 } }这个ScalableTile和普通Tile几乎一样只是多了一个baseScale用于在Unity编辑器里预设一个大小。真正的动态缩放系数不会存储在这里因为Tile是ScriptableObject资产同一资产被多个格子使用时修改其字段会影响所有实例。动态数据应该存储在运行时脚本中。3.2 核心控制器TileScaleController我们将创建一个名为TileScaleController的MonoBehaviour脚本把它挂载到你的Tilemap GameObject上。它是整个缩放系统的大脑负责管理哪些格子上的棋子需要缩放以及缩放倍数。在每帧渲染前将计算好的变换矩阵设置给Tilemap。using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public class TileScaleController : MonoBehaviour { private Tilemap _targetTilemap; private DictionaryVector3Int, float _scaleData new DictionaryVector3Int, float(); private DictionaryVector3Int, Matrix4x4 _matrixCache new DictionaryVector3Int, Matrix4x4(); private bool _isDirty false; // 标记数据是否变化用于优化 void Start() { _targetTilemap GetComponentTilemap(); if (_targetTilemap null) { Debug.LogError(TileScaleController must be attached to a GameObject with a Tilemap component.); enabled false; return; } // 关键订阅Tilemap在发送给渲染器之前的事件 _targetTilemap.GetComponentTilemapRenderer().RegisterSpriteBatchCallback(UpdateTileMatrices); } void OnDestroy() { if (_targetTilemap ! null) { var renderer _targetTilemap.GetComponentTilemapRenderer(); if (renderer ! null) { renderer.UnregisterSpriteBatchCallback(UpdateTileMatrices); } } } // 公共方法设置或更新某个格子的缩放 public void SetTileScale(Vector3Int cellPosition, float scale) { if (_targetTilemap null) return; // 检查该位置是否是我们定义的可缩放Tile TileBase tile _targetTilemap.GetTile(cellPosition); if (!(tile is ScalableTile)) return; scale Mathf.Max(0.1f, scale); // 防止缩放过小 if (_scaleData.ContainsKey(cellPosition)) { if (Mathf.Approximately(_scaleData[cellPosition], scale)) return; _scaleData[cellPosition] scale; } else { _scaleData.Add(cellPosition, scale); } _isDirty true; // 标记数据已更新 _matrixCache.Remove(cellPosition); // 清除该位置的缓存矩阵下次重新计算 } // 公共方法获取某个格子的缩放 public float GetTileScale(Vector3Int cellPosition) { return _scaleData.TryGetValue(cellPosition, out float scale) ? scale : 1.0f; } // 公共方法清除某个格子的缩放恢复为1 public void ClearTileScale(Vector3Int cellPosition) { if (_scaleData.Remove(cellPosition)) { _isDirty true; _matrixCache.Remove(cellPosition); } } // 核心回调在Tilemap渲染前被调用用于注入自定义矩阵 private void UpdateTileMatrices(SpriteBatch spriteBatch) { if (!_isDirty _matrixCache.Count 0) return; // 数据无变化且已有缓存跳过 // 获取Tilemap内部用于渲染的“区块”数据 var chunkData spriteBatch.GetChunkData(); // 这里是一个简化说明。实际我们需要遍历所有受影响的格子。 // 更高效的实现是只更新_isDirty为true时涉及的格子。 // 以下为概念性循环 foreach (var kvp in _scaleData) { Vector3Int cell kvp.Key; float scale kvp.Value; if (scale 1.0f) continue; // 缩放为1无需特殊处理 // 计算世界空间中心点 Vector3 worldPos _targetTilemap.GetCellCenterWorld(cell); // 创建以自身为中心缩放的变换矩阵 Matrix4x4 customMatrix Matrix4x4.TRS( worldPos, Quaternion.identity, new Vector3(scale, scale, 1f) ); // 关键步骤将这个自定义矩阵设置给这个格子对应的渲染数据。 // 我们需要找到spriteBatch中对应这个cell的渲染项。 // 此处涉及对SpriteBatch内部数据的查找和修改需要用到更底层的方法。 _matrixCache[cell] customMatrix; // 缓存起来 // 实际设置矩阵的代码需要在SpriteBatch的API中操作见下一节 } _isDirty false; } }上面的代码勾勒出了框架但最关键的一步——UpdateTileMatrices中如何将矩阵设置给渲染器——我们用了注释。因为直接操作SpriteBatch是相对底层的操作Unity的公开API没有直接提供这个方法。我们需要用到反射或者自定义Shader来实现。这里提供两种主流且更实用的实现路径。4. 完整代码实现两种实战方案由于直接通过C#代码注入矩阵到内置渲染流程比较晦涩我提供两种经过实践验证、更清晰的方案。方案一利用MaterialPropertyBlock方案二使用自定义渲染通道。4.1 方案一使用MaterialPropertyBlock传递缩放数据推荐这个方案的核心思想是不直接修改每个瓦片的变换矩阵而是通过Shader根据瓦片的世界位置动态计算缩放。我们在控制器里计算好每个需要缩放的格子的“缩放值”和“位置”然后通过MaterialPropertyBlock以数组形式传递给Tilemap的材质。Shader根据当前渲染的像素是否在目标格子范围内来决定是否应用缩放。步骤1修改TileScaleControllerusing System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public class TileScaleController : MonoBehaviour { public Tilemap targetTilemap; private MaterialPropertyBlock _propertyBlock; private static readonly int ScaleDataArrayID Shader.PropertyToID(_ScaleData); private static readonly int ScaleDataLengthID Shader.PropertyToID(_ScaleDataLength); private static readonly int CellSizeID Shader.PropertyToID(_CellSize); [System.Serializable] public struct TileScaleInfo { public Vector3 worldPosition; // 格子中心世界坐标 public float scale; } public ListTileScaleInfo activeScaledTiles new ListTileScaleInfo(); private DictionaryVector3Int, int _tileIndexMap new DictionaryVector3Int, int(); void Start() { if (targetTilemap null) targetTilemap GetComponentTilemap(); _propertyBlock new MaterialPropertyBlock(); targetTilemap.GetPropertyBlock(_propertyBlock); // 获取现有的 UpdateShaderData(); } void Update() { // 每帧更新MaterialPropertyBlock确保数据同步 if (targetTilemap.HasPropertyBlock()) { targetTilemap.GetPropertyBlock(_propertyBlock); } UpdateShaderData(); targetTilemap.SetPropertyBlock(_propertyBlock); } public void SetTileScale(Vector3Int cellPosition, float scale) { Vector3 worldPos targetTilemap.GetCellCenterWorld(cellPosition); if (_tileIndexMap.TryGetValue(cellPosition, out int index)) { // 更新已有项 var info activeScaledTiles[index]; info.scale scale; activeScaledTiles[index] info; } else { // 新增项 activeScaledTiles.Add(new TileScaleInfo { worldPosition worldPos, scale scale }); _tileIndexMap[cellPosition] activeScaledTiles.Count - 1; } } public void ClearTileScale(Vector3Int cellPosition) { if (_tileIndexMap.TryGetValue(cellPosition, out int index)) { // 简单做法将缩放设为1而不是移除避免数组索引错乱。 // 更健壮的做法是使用List并管理索引这里为简化用前者。 var info activeScaledTiles[index]; info.scale 1.0f; activeScaledTiles[index] info; _tileIndexMap.Remove(cellPosition); } } private void UpdateShaderData() { if (_propertyBlock null) return; // 将List数据转换为Shader能接受的数组 Vector4[] scaleDataArray new Vector4[activeScaledTiles.Count]; for (int i 0; i activeScaledTiles.Count; i) { var info activeScaledTiles[i]; // Vector4的xyzw存储: (世界位置.x, 世界位置.y, 缩放, 0) scaleDataArray[i] new Vector4(info.worldPosition.x, info.worldPosition.y, info.scale, 0); } _propertyBlock.SetVectorArray(ScaleDataArrayID, scaleDataArray); _propertyBlock.SetInt(ScaleDataLengthID, scaleDataArray.Length); // 传递格子大小用于在Shader中判断像素属于哪个格子 Vector3 cellSize targetTilemap.cellSize; _propertyBlock.SetVector(CellSizeID, new Vector4(cellSize.x, cellSize.y, 0, 0)); } }步骤2编写自定义ShaderUnlit Shader Graph 或 Surface Shader这里以Shader Graph为例说明原理。你需要创建一个Unlit Shader Graph。暴露参数在Graph的Blackboard中创建三个属性_ScaleData(类型Vector4 Array)_ScaleDataLength(类型Integer)_CellSize(类型Vector2)流程设计获取像素世界位置使用Position节点空间选择World。循环判断使用For Loop节点循环次数为_ScaleDataLength。在循环体内通过Array Element节点用循环的Index获取_ScaleData数组中的一项一个Vector4。从该项中提取.xy作为目标格子中心世界坐标提取.z作为缩放系数。计算当前像素世界位置与目标格子中心的绝对距离Absolute(Distance)。判断这个距离是否小于半个格子大小_CellSize * 0.5。这里可以稍微加点容差。如果判断为真则说明当前像素位于需要缩放的格子内。我们可以计算一个基于距离的平滑权重可选然后利用这个权重对UV坐标进行反向缩放因为放大图像实际是缩小UV采样范围或者更直接地计算一个偏移向量。应用UV变换将计算出的UV偏移或缩放因子应用到Texture Sample节点的UV输入上。实操心得在Shader中精确匹配格子边界可能因为浮点数精度产生接缝。一个常见的技巧是在判断时使用step或smoothstep函数并给予一个很小的容差值如0.01或者直接基于像素世界位置对齐到网格坐标再进行判断这样更精确。方案一优缺点优点逻辑清晰与Tilemap标准渲染流程兼容性好不需要深入底层渲染API。缺点需要在Shader中做每像素的距离判断如果需要缩放的格子很多可能会增加Shader计算复杂度。传递数组有长度限制具体取决于平台和Unity版本。4.2 方案二使用CommandBuffer进行后期缩放更灵活方案二思路完全不同我们不在Tilemap渲染时修改每个瓦片而是在整个Tilemap渲染到一张中间纹理后再通过一个后期处理对纹理上特定的区域对应棋盘格子进行缩放。这更像一个全屏后处理效果但只针对特定坐标。步骤1创建缩放后期处理脚本using UnityEngine; using UnityEngine.Rendering; public class TileScalePostProcessing : MonoBehaviour { public TileScaleController scaleController; // 关联之前的控制器 public Material scaleMaterial; // 一个特殊的材质用于屏幕局部缩放 private CommandBuffer _commandBuffer; private Camera _mainCamera; void OnEnable() { _mainCamera Camera.main; if (_mainCamera null) return; _commandBuffer new CommandBuffer { name Tile Scale Overlay }; // 假设scaleMaterial有一个Pass它能读取_ScaledTiles等纹理并做处理 // 这里需要scaleController把缩放格子信息渲染到一张RenderTexture上 _commandBuffer.Blit(null, BuiltinRenderTextureType.CameraTarget, scaleMaterial); _mainCamera.AddCommandBuffer(CameraEvent.AfterForwardOpaque, _commandBuffer); } void OnDisable() { if (_mainCamera ! null _commandBuffer ! null) { _mainCamera.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, _commandBuffer); } _commandBuffer?.Release(); } void Update() { // 每帧更新scaleMaterial所需的参数例如传递scaleController.activeScaledTiles数组 if (scaleMaterial ! null scaleController ! null) { // 将缩放信息位置、缩放值打包到一个Texture2D或ComputeBuffer传递给Shader // 这部分是数据传输的具体实现略复杂 } } }步骤2编写后期处理Shader这个Shader是一个全屏Shader。它接收一张由Tilemap渲染的_MainTex即Camera的源纹理。一个包含了所有缩放格子信息位置、半径、缩放强度的缓冲区。在Fragment Shader中对于每一个屏幕像素将其UV转换为世界坐标。遍历所有缩放格子信息。如果该像素在世界坐标上位于某个缩放格子的影响范围内则计算一个新的UV坐标。这个新UV是相对于该格子中心进行缩放的(originalUV - centerUV) / scale centerUV。用新的UV去采样_MainTex得到缩放后的颜色。方案二优缺点优点极其灵活可以实现各种复杂的屏幕空间变形效果不局限于Tilemap任何SpriteRenderer都能生效。性能开销相对恒定与屏幕分辨率相关与缩放格子数量关系不大。缺点实现更复杂涉及CommandBuffer、RenderTexture管理需要处理渲染顺序和透明混合问题。如果多个物体叠加后期处理可能难以区分层级。4.3 方案选择与代码整合建议对于大多数2D棋盘游戏方案一MaterialPropertyBlock Shader是更推荐的选择。它更贴近Tilemap的渲染本质性能可预测且不会引入后期处理可能带来的渲染顺序问题。方案二更适合需要全局、非线性变形特效的场景。这里给出方案一的简化版整合示例假设我们使用一个预设好的Shader它已经实现了根据_ScaleDataArray进行格子内缩放的功能。// 附加到Tilemap上的完整控制器简化版 using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; [RequireComponent(typeof(Tilemap))] public class SimpleTileScaleController : MonoBehaviour { public Material scaledTileMaterial; // 使用了我们自定义Shader的材质球 private Tilemap _tilemap; private MaterialPropertyBlock _mpb; private ListVector4 _scaleDataList new ListVector4(); private int _scaleDataID; private int _dataLengthID; void Awake() { _tilemap GetComponentTilemap(); _mpb new MaterialPropertyBlock(); _scaleDataID Shader.PropertyToID(_ScaleData); _dataLengthID Shader.PropertyToID(_DataLength); // 确保Tilemap使用我们的特殊材质 var renderer _tilemap.GetComponentTilemapRenderer(); if (scaledTileMaterial ! null) { renderer.material scaledTileMaterial; } } void LateUpdate() { UpdatePropertyBlock(); } public void AddOrUpdateScale(Vector3Int cell, float scale) { Vector3 worldPos _tilemap.GetCellCenterWorld(cell); // 查找是否已存在 for(int i 0; i _scaleDataList.Count; i) { Vector4 data _scaleDataList[i]; if (Vector2.Distance(new Vector2(data.x, data.y), new Vector2(worldPos.x, worldPos.y)) 0.1f) { data.z scale; _scaleDataList[i] data; return; } } // 新增 _scaleDataList.Add(new Vector4(worldPos.x, worldPos.y, scale, 0)); } public void RemoveScale(Vector3Int cell) { Vector3 worldPos _tilemap.GetCellCenterWorld(cell); for (int i _scaleDataList.Count - 1; i 0; i--) { Vector4 data _scaleDataList[i]; if (Vector2.Distance(new Vector2(data.x, data.y), new Vector2(worldPos.x, worldPos.y)) 0.1f) { _scaleDataList.RemoveAt(i); break; } } } private void UpdatePropertyBlock() { if (_tilemap null) return; _tilemap.GetPropertyBlock(_mpb); _mpb.SetInt(_dataLengthID, _scaleDataList.Count); if (_scaleDataList.Count 0) { _mpb.SetVectorArray(_scaleDataID, _scaleDataList.ToArray()); } _tilemap.SetPropertyBlock(_mpb); } }这个简化版在LateUpdate中持续更新属性块。在实际项目中你应该在缩放值发生变化时才调用UpdatePropertyBlock()以避免不必要的每帧更新。5. 应用场景与效果扩展实现了基础缩放后你可以轻松地将它融入到游戏逻辑中创造出丰富的交互效果。5.1 动态交互效果棋子选中高亮当玩家点击或鼠标悬停在一个单位上时调用SetTileScale(cell, 1.2f)使其微微放大配合颜色变化提示选中状态。取消选中时调用ClearTileScale。// 在玩家的输入控制脚本中 void HandleClick(Vector3 worldPos) { Vector3Int cell _gameGridTilemap.WorldToCell(worldPos); if (IsValidUnitCell(cell)) { // 先清除上一个选中的 if (_lastSelectedCell ! null) _scaleController.ClearTileScale(_lastSelectedCell.Value); // 放大当前选中的 _scaleController.SetTileScale(cell, 1.2f); _lastSelectedCell cell; } }技能范围指示器当玩家选择一个技能时计算出技能影响的范围格子列表然后遍历这些格子将其中的棋子瓦片放大并改变颜色通过修改Tilemap的SetColor或Shader中的颜色参数清晰展示作用目标。public void HighlightArea(ListVector3Int areaCells, float scale, Color tintColor) { foreach (var cell in areaCells) { _scaleController.SetTileScale(cell, scale); _tilemap.SetColor(cell, tintColor); // 同时修改颜色 } }状态变化反馈当单位获得“巨大化”Buff时持续将其缩放设为1.5f当处于“缩小”Debuff时设为0.8f。Buff结束时恢复为1.0f。5.2 结合动画系统单纯的跳变缩放会很生硬。我们可以结合Mathf.Lerp或DOTween等动画插件实现平滑的缩放动画。using DG.Tweening; // 使用DOTween插件 public void AnimateTileScale(Vector3Int cell, float targetScale, float duration) { float currentScale _scaleController.GetTileScale(cell); DOTween.To(() currentScale, x _scaleController.SetTileScale(cell, x), targetScale, duration) .SetEase(Ease.OutBack); // 添加一个回弹效果 }这样棋子的放大缩小就会带有平滑的动画视觉体验大幅提升。5.3 视觉与逻辑的同步问题正如之前提到的这只是视觉缩放。棋子的逻辑位置cell和碰撞体如果Tilemap Collider 2D使用了此瓦片并没有改变。这通常是我们期望的——棋子逻辑上还在那个格子里只是看起来大了。但有些情况下你需要同步自定义碰撞检测如果你需要根据放大后的视觉大小进行更精确的碰撞如鼠标点选你不能再依赖格子坐标。你需要根据缩放后的瓦片世界边界单元格中心 ± (单元格大小 * 缩放系数 / 2)来进行自定义的物理检测如Physics2D.OverlapPoint。UI附着如果棋子头顶有血条、名字等UI你需要让UI跟随棋子视觉中心移动。这时需要计算缩放后瓦片的视觉中心世界坐标它其实和逻辑中心GetCellCenterWorld是一样的因为我们是均匀缩放。但如果你未来实现了非均匀缩放或旋转就需要用矩阵变换去计算顶点的世界位置了。6. 性能优化与常见问题排查6.1 性能优化要点减少每帧更新只在缩放数据真正发生变化时更新MaterialPropertyBlock或CommandBuffer。使用_isDirty这样的标志位来控制。控制数据量方案一中传递给Shader的数组不宜过大。如果一屏内需要动态缩放的棋子很多比如超过50个需要考虑分帧更新或使用更高效的数据结构如ComputeBuffer。Shader优化在Shader的循环中尽早使用break一旦找到匹配的格子就跳出循环。考虑将格子信息按照空间分区如网格传递给Shader减少每个像素需要遍历的数量。对于移动设备复杂的逐像素判断可能会成为瓶颈需进行性能测试。批处理破坏使用MaterialPropertyBlock本身不会破坏Tilemap的静态合批但如果你为Tilemap设置了不同的材质实例material instance则可能会造成批处理中断。确保所有使用此缩放系统的Tilemap尽量共享材质实例。6.2 常见问题与解决方案问题1缩放后瓦片边缘出现接缝或闪烁。原因Shader中判断像素是否属于某个格子的逻辑不精确存在浮点数误差导致边缘像素在“属于”和“不属于”之间摇摆。解决在距离判断时增加一个小的容差epsilon例如0.005。或者更稳健的方法是将像素的世界坐标反向转换为单元格坐标WorldToCell然后判断该单元格坐标是否在需要缩放的列表中这样判断是离散且精确的。但这需要在Shader中访问Tilemap的变换信息实现稍复杂。问题2缩放时瓦片看起来被“拉伸”了而不是以中心点放大。原因缩放矩阵的原点不对。确保你使用的是Matrix4x4.TRS并且平移参数位置是该格子中心的世界坐标。如果你错误地使用了格子左下角或其他点作为原点就会出现拉伸。解决再次确认worldPos的计算是tilemap.GetCellCenterWorld(cellPosition)。问题3在移动设备上帧率明显下降。原因每帧更新的格子太多或Shader计算过于复杂。解决进行性能剖析Profiler查看是CPU端更新数据还是GPU端Shader的瓶颈。限制同时具有动态缩放效果的棋子数量。简化Shader减少循环次数和复杂运算。可以考虑将缩放效果“烘焙”成简单的Sprite动画对于固定模式的缩放如选中态用Animation Tile替代动态计算。问题4缩放效果对某些自定义的Tile如Rule Tile、Animated Tile不生效。原因自定义Tile可能重写了GetTileData方法返回了不同的Sprite或数据。我们的缩放系统依赖于特定的Tile类型如ScalableTile或通过位置信息判断。解决在SetTileScale方法中不要检查Tile类型或者扩展检查逻辑将你需要支持的所有Tile类型都加入白名单。更好的设计是不依赖Tile类型而是维护一个独立于Tile数据的“格子状态字典”只关心位置。问题5如何实现非均匀缩放只拉宽或拉高解决非常简单。在创建缩放矩阵或传递缩放数据时将Vector3的x和y分量设置为不同的值即可。例如new Vector3(1.5f, 1.0f, 1f)表示只横向放大1.5倍。在Shader中处理时也需要分别对UV的x和y分量进行不同的缩放计算。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价