Unity Tilemap动态缩放:基于Matrix4x4实现单个瓦片独立变换 1. 项目概述当Tilemap遇上动态缩放在Unity里做2D游戏尤其是战棋、策略或者一些需要精细网格管理的类型Tilemap几乎是绕不开的核心组件。它把地图变成了一个由标准“瓦片”组成的网格管理起来方便性能也好。但不知道你有没有遇到过这样的需求地图上的某个“棋子”比如一个单位、一个建筑或者一个特殊事件标记它本身也是一个Tile但你需要它在特定情况下能动态改变大小——比如角色升级后图标变大显得更威武或者某个可交互物体被选中时需要高亮放大。这时候问题就来了。Unity的Tilemap系统在设计上一个Tile的尺寸由Grid组件的Cell Size决定是全局统一的。你没法直接通过修改某个Tile的transform.localScale来让它单独变大变小因为Tile的渲染是由Tilemap组件批量处理的不遵循常规GameObject的变换层级。网上常见的“解决方案”是把这个棋子做成一个独立的SpriteRenderer脱离Tilemap系统。但这又带来了新的麻烦你需要额外处理这个Sprite的排序层级Order in Layer来让它和Tilemap完美融合对齐网格位置也变得繁琐失去了Tilemap自带的管理便利性。所以我们今天的核心目标就是在不破坏Tilemap统一管理的前提下实现单个Tile的动态缩放。这听起来像是个“不可能的任务”但Unity强大的底层图形API给了我们一把钥匙Matrix4x4。通过直接操作渲染矩阵我们可以“欺骗”GPU让它按照我们的意愿去绘制某个特定的瓦片。这篇文章我将带你从原理到实践手把手实现这个功能并附上可直接复用的完整代码。无论你是正在为战棋游戏的角色大小发愁还是想在地图编辑器中实现更灵活的视觉效果这套方案都能给你提供一个坚实可靠的思路。2. 核心原理为什么是Matrix4x4在深入代码之前我们必须先搞懂Matrix4x4在这里扮演的角色。你可以把它理解为一个“变形指令集”。在计算机图形学中一个物体从模型本身的坐标模型空间经过旋转、缩放、平移合称变换最终被画到屏幕上的过程就是通过矩阵乘法来完成的。Unity的渲染管线默认会为每个Renderer比如SpriteRenderer计算一个“模型-视图-投影矩阵”MVP Matrix。对于TilemapUnity会为每一帧、每一个Tile计算这个矩阵其中就包含了该Tile在世界空间中的位置由Tilemap的Grid布局决定和其本身的缩放通常是1,1,1。然后这个矩阵被传递给ShaderShader用它来定位和渲染瓦片。我们的突破口就在这里Unity的Tilemap渲染允许我们提供一个自定义的“每实例数据”Per-Instance Data。Tilemap组件在渲染时会为每个Tile查询一次。我们可以通过脚本在查询时动态地返回一个修改过的Matrix4x4这个矩阵包含了我们想要的额外缩放甚至旋转、平移信息。Unity的渲染器会使用我们提供的这个矩阵而不是它自己计算的那个默认矩阵从而实现了对单个Tile的独立变换控制。为什么这个方法比替代方案好性能无损它依然在Tilemap的合批Batching体系内。Tilemap的静态合批效率极高我们的修改是在合批之前注入数据不会打断合批过程。管理便捷棋子仍然在Tilemap的坐标系下。它的位置由Tilemap的网格坐标Vector3Int唯一确定移动棋子只需要改变其网格坐标逻辑清晰。功能强大Matrix4x4不仅能处理缩放还能处理旋转和非均匀缩放比如拉长为未来功能扩展留足了空间。理解了“为什么”之后我们来看看“怎么做”。整个实现流程可以拆解为三个关键环节数据存储、矩阵计算与注入、以及一个高效的查询管理器。3. 系统设计与关键数据结构要实现动态缩放我们首先要解决一个问题Unity的Tilemap系统在渲染时如何知道该去缩放哪个Tile以及缩放多少我们需要一个配套的数据管理系统来存储和提供这些信息。3.1 定义缩放数据容器TileData我们不能直接把缩放值挂在Tile Asset上因为一个Tile Asset如一个“士兵”瓦片可能被用在多个地方。我们需要的是基于位置网格坐标的实例数据。因此我设计了一个TileData类来存储这些信息。using UnityEngine; using System.Collections.Generic; /// summary /// 存储单个Tile实例的变换数据 /// /summary [System.Serializable] public class TileData { public Vector3Int position; // 在Tilemap中的网格坐标 public Vector3 scale Vector3.one; // 该Tile的缩放值默认是(1,1,1) // 未来可以扩展 rotation, offset 等字段 }这个类很简单它把一个网格坐标和一个缩放向量绑定在一起。Vector3.one就是(1,1,1)表示原始大小。3.2 构建数据管理器TileTransformManager有了数据容器我们需要一个中心化的管理器来增删改查这些数据。这个管理器将以Tilemap为单位进行工作。using UnityEngine; using UnityEngine.Tilemaps; using System.Collections.Generic; /// summary /// 管理指定Tilemap上所有需要动态变换的Tile数据 /// 必须挂载在与Tilemap相同的GameObject上 /// /summary [RequireComponent(typeof(Tilemap))] public class TileTransformManager : MonoBehaviour { private Tilemap _targetTilemap; private DictionaryVector3Int, TileData _tileDataDict; private void Awake() { _targetTilemap GetComponentTilemap(); _tileDataDict new DictionaryVector3Int, TileData(); } /// summary /// 设置或更新某个位置Tile的缩放 /// /summary /// param nameposition网格坐标/param /// param namescale缩放值如 (2,2,1) 表示放大两倍/param public void SetTileScale(Vector3Int position, Vector3 scale) { if (_tileDataDict.ContainsKey(position)) { _tileDataDict[position].scale scale; } else { _tileDataDict[position] new TileData { position position, scale scale }; } // 关键通知Tilemap刷新该位置触发重新查询渲染数据 _targetTilemap.RefreshTile(position); } /// summary /// 获取某个位置Tile的缩放数据如果没有则返回默认值(1,1,1) /// /summary public Vector3 GetTileScale(Vector3Int position) { if (_tileDataDict.TryGetValue(position, out TileData data)) { return data.scale; } return Vector3.one; } /// summary /// 移除某个位置的变换数据恢复默认大小 /// /summary public void RemoveTileData(Vector3Int position) { if (_tileDataDict.Remove(position)) { _targetTilemap.RefreshTile(position); } } /// summary /// 提供给TilemapRenderer查询的关键方法 /// /summary public bool TryGetTileData(Vector3Int position, out TileData data) { return _tileDataDict.TryGetValue(position, out data); } }核心方法解析SetTileScale: 这是外部调用的主要接口。你传入网格坐标和缩放值它会更新字典并调用_targetTilemap.RefreshTile(position)。这个调用至关重要它告诉Unity的Tilemap系统“这个位置的Tile数据可能变了请重新向我查询一下怎么画它”。没有这一步修改不会立即生效。TryGetTileData: 这是一个internal或public的方法它将被我们接下来要创建的渲染查询组件调用。当Tilemap渲染每一帧时会通过这个方法来获取指定位置是否有自定义的变换数据。注意事项一坐标一致性确保你传递和获取的Vector3Int坐标与你使用_targetTilemap.SetTile(position, tileAsset)设置瓦片时使用的坐标是同一个坐标系。通常就是世界坐标转换而来的网格坐标_targetTilemap.WorldToCell(worldPosition)。3.3 创建自定义Tile基类TransformableTile虽然数据管理器和查询逻辑准备好了但Tilemap渲染时默认并不会主动去调用我们的管理器。我们需要一个“钩子”。在Unity中自定义的Tile类可以重写GetTileData方法这个方法正是在Tilemap刷新或渲染某个位置时被调用的。我们可以在这里插入我们的逻辑。不过这里有一个更高效且推荐的做法不直接创建复杂的Tile子类而是利用Tilemap的GetTileData回调机制。Unity的TilemapRenderer组件有一个detectChunkCullingBounds选项并且支持通过Tilemap的GetTileData事件来获取每Tile的数据。但更直接的方式是使用Tilemap的GetTileData方法的覆写这需要继承Tilemap。为了保持简单和兼容性我们将采用另一种标准做法——创建一个自定义的Tile资产并在其GetTileData方法中将查询请求转发给管理器。首先创建一个空的、仅用于标记的Tile类using UnityEngine; using UnityEngine.Tilemaps; /// summary /// 一个“占位符”Tile其唯一作用是在GetTileData被调用时 /// 将请求转发给关联的TileTransformManager。 /// 你需要为每个需要缩放的棋子创建并使用这个Tile Asset。 /// /summary [CreateAssetMenu(fileName New Transformable Tile, menuName 2D/Tiles/Transformable Tile)] public class TransformableTile : TileBase { // 这个Tile本身没有外观外观由关联的Sprite决定。 // 它的核心逻辑在GetTileData中。 public Sprite sprite; public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData) { // 先设置基础数据如图片 tileData.sprite sprite; tileData.color Color.white; tileData.transform Matrix4x4.identity; // 先设为默认单位矩阵 tileData.flags TileFlags.LockTransform; // 锁定变换告诉Unity我们会提供自定义矩阵 // 关键找到挂载在同一个GameObject上的TileTransformManager Tilemap targetTilemap tilemap.GetComponentTilemap(); if (targetTilemap ! null) { TileTransformManager manager targetTilemap.GetComponentTileTransformManager(); if (manager ! null manager.TryGetTileData(position, out TileData customData)) { // 如果管理器中有该位置的自定义数据则计算并应用变换矩阵 // 注意这里只是获取到缩放值矩阵计算将在渲染时由另一个组件完成。 // 我们这里需要做的是通过某种方式将缩放值传递出去。 // 一个更简洁的方案是不在这里计算而是让一个渲染器组件直接监听所有Tile的GetTileData调用。 } } } }但上述方法在传递数据上会遇到架构复杂度的挑战。一个更清晰、更符合Unity Tilemap渲染流程的标准做法是使用Tilemap的附加渲染数据功能。不过这需要用到Tilemap的SetColor、SetTransformMatrix等接口而这些接口会影响整个Tilemap的渲染批次。因此我们采用最终方案不修改Tile基类而是完全通过TileTransformManager和另一个渲染拦截组件TileTransformRenderer来协作。TransformableTile可以就是一个普通的Tile或RuleTile我们只关心它的位置。管理器记录位置和缩放值的对应关系渲染器在Unity渲染流程的合适时机为这些特定位置的Tile应用变换矩阵。4. 核心实现Matrix4x4的计算与注入这是整个系统的引擎室。我们需要一个组件在每一帧渲染前遍历所有被标记的Tile位置计算出自定义矩阵并“注入”到Tilemap的渲染数据中。4.1 创建渲染器组件TileTransformRenderer这个组件需要和TileTransformManager挂载在同一个GameObject上即同一个Tilemap对象上。它负责在LateUpdate或特定的渲染回调中执行矩阵计算和设置。using UnityEngine; using UnityEngine.Tilemaps; /// summary /// 负责将TileTransformManager中存储的缩放数据通过Matrix4x4应用到Tilemap的渲染中。 /// 必须与TileTransformManager挂载在同一GameObject上。 /// /summary [RequireComponent(typeof(Tilemap), typeof(TileTransformManager))] public class TileTransformRenderer : MonoBehaviour { private Tilemap _tilemap; private TileTransformManager _dataManager; private void Awake() { _tilemap GetComponentTilemap(); _dataManager GetComponentTileTransformManager(); } private void LateUpdate() { // 每帧更新所有需要变换的Tile ApplyTileTransforms(); } /// summary /// 核心方法遍历所有有记录的位置计算并设置变换矩阵 /// /summary private void ApplyTileTransforms() { // 注意这里我们需要一个方式来获取_all_被管理的位置。 // 我们需要修改TileTransformManager使其能提供一个位置列表或迭代器。 // 为了示例清晰我们先假设Manager有一个GetAllPositions方法。 // 实际代码中我们可以在Manager内部维护一个位置列表或直接遍历字典。 // 临时方案在Manager中添加一个公共属性或方法暴露数据字典的键集合。 // 我们先在Manager里加一个public IEnumerableVector3Int GetManagedPositions() _tileDataDict.Keys; } }我们需要修改TileTransformManager提供一个获取所有被管理坐标的方法// 在TileTransformManager类中添加 public IEnumerableVector3Int GetManagedPositions() { return _tileDataDict.Keys; }现在完善ApplyTileTransforms方法private void ApplyTileTransforms() { foreach (Vector3Int pos in _dataManager.GetManagedPositions()) { Vector3 scale _dataManager.GetTileScale(pos); // 计算缩放矩阵 // Matrix4x4.Scale 创建一个缩放矩阵 Matrix4x4 scaleMatrix Matrix4x4.Scale(scale); // 关键步骤将缩放矩阵设置到Tilemap的该位置 // Tilemap.SetTransformMatrix 正是用于设置每个Tile独立变换矩阵的API _tilemap.SetTransformMatrix(pos, scaleMatrix); } // 重要清理那些已经被移除缩放为(1,1,1)但矩阵可能还被缓存的位置 // 实际上当调用RemoveTileData时我们已经Refresh了Tile但矩阵可能未被清除。 // 更稳健的做法是在Manager中记录“脏”状态或者每帧重置所有被管理位置的矩阵。 // 另一种思路不遍历管理器而是遍历所有有Tile的位置检查其是否需要缩放。 // 但遍历所有Tile可能效率较低。我们采用“设置后不清除”的策略因为将缩放设为(1,1,1)即恢复原样。 }这里有一个重大优化和问题修复SetTransformMatrix这个API会为指定位置设置一个持久的变换矩阵。如果我们只在LateUpdate中为有数据的Tile设置矩阵那么当一个Tile的数据被移除RemoveTileData后它的矩阵仍然保留着上一次设置的值不会自动恢复为单位矩阵。这会导致“幽灵缩放”。解决方案我们需要在每一帧开始时重置所有需要被管理的Tile的矩阵然后再应用新的缩放。或者我们维护一个“上一帧被管理的位置”集合将这一帧不再被管理的位置的矩阵重置。后一种更高效。修改TileTransformRendererpublic class TileTransformRenderer : MonoBehaviour { private Tilemap _tilemap; private TileTransformManager _dataManager; private HashSetVector3Int _lastFramePositions new HashSetVector3Int(); private void Awake() { /* ... 同上 ... */ } private void LateUpdate() { // 获取当前帧所有需要变换的位置 HashSetVector3Int currentPositions new HashSetVector3Int(_dataManager.GetManagedPositions()); // 1. 重置上一帧有、但这一帧没有的位置的矩阵恢复原状 foreach (Vector3Int oldPos in _lastFramePositions) { if (!currentPositions.Contains(oldPos)) { // 重置为单位矩阵 _tilemap.SetTransformMatrix(oldPos, Matrix4x4.identity); } } // 2. 为当前帧所有需要变换的位置设置矩阵 foreach (Vector3Int pos in currentPositions) { Vector3 scale _dataManager.GetTileScale(pos); // 只有当缩放值不是(1,1,1)时才需要设置矩阵吗是的但管理器里存储的可能就是(1,1,1)。 // 我们可以选择跳过但为了逻辑统一即使为(1,1,1)也设置一个单位矩阵。 // 但注意单位矩阵和“未设置矩阵”在渲染结果上是一样的但通过SetTransformMatrix设置后它就占用了这个“自定义矩阵槽位”。 if (scale ! Vector3.one) { _tilemap.SetTransformMatrix(pos, Matrix4x4.Scale(scale)); } else { // 如果缩放是1也显式设置为单位矩阵确保覆盖掉可能存在的旧矩阵 _tilemap.SetTransformMatrix(pos, Matrix4x4.identity); } } // 3. 更新上一帧位置记录 _lastFramePositions currentPositions; } }这个方案更加健壮确保了状态的一致性。Matrix4x4.identity就是单位矩阵表示没有变换。4.2 矩阵计算的细节与陷阱缩放中心点问题默认情况下Matrix4x4.Scale(scale)创建的缩放矩阵是以**世界原点(0,0,0)**为中心进行缩放的。这显然不对我们希望Tile围绕它自身的中心或者说它所在的网格单元的中心进行缩放。因此我们需要构造一个“平移-缩放-平移回去”的复合变换矩阵。这个过程称为“以某点为中心缩放”。先将Tile平移到世界原点应用一个平移矩阵T1。在世界原点进行缩放应用缩放矩阵S。再将Tile平移回原来的位置应用平移矩阵T2它是T1的逆矩阵。矩阵乘法顺序是反的从右往左应用所以最终的矩阵M T2 * S * T1。在代码中实现private Matrix4x4 GetScaleMatrixAtCell(Vector3Int cellPosition, Vector3 scale) { // 获取该网格单元的世界空间中心坐标 Vector3 cellCenterWorld _tilemap.GetCellCenterWorld(cellPosition); // 构造矩阵 Matrix4x4 translationToOrigin Matrix4x4.Translate(-cellCenterWorld); Matrix4x4 scaleMatrix Matrix4x4.Scale(scale); Matrix4x4 translationBack Matrix4x4.Translate(cellCenterWorld); // 矩阵乘法顺序先平移至原点再缩放再平移回去 return translationBack * scaleMatrix * translationToOrigin; }然后在LateUpdate循环中使用这个方法来获取矩阵Matrix4x4 transformMatrix GetScaleMatrixAtCell(pos, scale); _tilemap.SetTransformMatrix(pos, transformMatrix);注意事项二性能考量每帧为每个动态Tile计算一次矩阵乘法如果动态Tile数量很多比如上百个可能会带来一定的CPU开销。在实际项目中如果缩放值不常变化可以进行优化在TileTransformManager的SetTileScale方法中直接计算好矩阵并存储起来TileTransformRenderer每帧只负责设置不重复计算。我们当前的方案是清晰优先优化可以在后续进行。5. 完整代码整合与使用示例让我们把上面的关键代码整合到一个更简洁、更易用的版本中。我们将TileTransformManager和TileTransformRenderer的功能合并到一个组件中简化流程。5.1 最终版组件DynamicTileScalerusing UnityEngine; using UnityEngine.Tilemaps; using System.Collections.Generic; /// summary /// 动态Tile缩放控制器。挂载到需要此功能的Tilemap对象上即可。 /// /summary [RequireComponent(typeof(Tilemap))] public class DynamicTileScaler : MonoBehaviour { private Tilemap _tilemap; private DictionaryVector3Int, Vector3 _tileScaleData new DictionaryVector3Int, Vector3(); private HashSetVector3Int _lastFrameScaledTiles new HashSetVector3Int(); private void Awake() { _tilemap GetComponentTilemap(); } private void LateUpdate() { ApplyTileScales(); } /// summary /// 设置或更新指定网格位置Tile的缩放。 /// /summary /// param namecellPosition网格坐标例如通过_tilemap.WorldToCell获取/param /// param namescale缩放值如Vector3.one*2表示放大两倍/param public void SetTileScale(Vector3Int cellPosition, Vector3 scale) { _tileScaleData[cellPosition] scale; // 立即刷新该Tile促使Tilemap重新获取渲染数据 _tilemap.RefreshTile(cellPosition); } public Vector3 GetTileScale(Vector3Int cellPosition) { return _tileScaleData.TryGetValue(cellPosition, out Vector3 scale) ? scale : Vector3.one; } public void ResetTileScale(Vector3Int cellPosition) { if (_tileScaleData.Remove(cellPosition)) { _tilemap.RefreshTile(cellPosition); } } /// summary /// 核心渲染逻辑每帧更新变换矩阵 /// /summary private void ApplyTileScales() { // 当前帧需要处理的Tile位置 HashSetVector3Int currentFramePositions new HashSetVector3Int(_tileScaleData.Keys); // 1. 清理将上一帧有、但这一帧没有的Tile的矩阵重置 foreach (Vector3Int oldPos in _lastFrameScaledTiles) { if (!currentFramePositions.Contains(oldPos)) { _tilemap.SetTransformMatrix(oldPos, Matrix4x4.identity); } } // 2. 设置为当前帧所有需要缩放的Tile计算并设置矩阵 foreach (Vector3Int pos in currentFramePositions) { Vector3 scale _tileScaleData[pos]; Matrix4x4 matrix (scale ! Vector3.one) ? CreateScaleMatrixAtCell(pos, scale) : Matrix4x4.identity; _tilemap.SetTransformMatrix(pos, matrix); } // 3. 记录本轮已处理的Tile供下一帧对比 _lastFrameScaledTiles currentFramePositions; } /// summary /// 创建一个以网格单元中心为原点的缩放矩阵 /// /summary private Matrix4x4 CreateScaleMatrixAtCell(Vector3Int cellPos, Vector3 scale) { Vector3 cellCenter _tilemap.GetCellCenterWorld(cellPos); // 构造 “T_back * S * T_to_origin” 矩阵 return Matrix4x4.Translate(cellCenter) * Matrix4x4.Scale(scale) * Matrix4x4.Translate(-cellCenter); } /// summary /// 辅助方法通过世界坐标设置缩放更易用 /// /summary public void SetTileScaleAtWorld(Vector3 worldPosition, Vector3 scale) { SetTileScale(_tilemap.WorldToCell(worldPosition), scale); } }5.2 如何使用这个组件准备场景在Unity中创建一个2D场景添加一个GridGameObject其下创建一个Tilemap。挂载组件选中你的TilemapGameObject将DynamicTileScaler脚本挂载上去。放置瓦片在Tile Palette中使用任意你喜欢的Tile或RuleTile在Tilemap上绘制一些棋子。不需要特殊的Tile Asset任何Tile都可以。编写控制逻辑在另一个控制角色、技能或UI的脚本中获取DynamicTileScaler组件并调用其方法。示例控制脚本using UnityEngine; public class ChessPieceController : MonoBehaviour { public Tilemap targetTilemap; // 拖拽你的Tilemap到这里 public Vector3Int piecePosition new Vector3Int(2, 3, 0); // 棋子的网格坐标 public float scaleDuration 0.5f; // 缩放动画时长 private DynamicTileScaler _scaler; private float _timer; private bool _isScalingUp true; void Start() { _scaler targetTilemap.GetComponentDynamicTileScaler(); if (_scaler null) { Debug.LogError(请在Tilemap上添加DynamicTileScaler组件); this.enabled false; } } void Update() { // 示例按空格键触发缩放动画 if (Input.GetKeyDown(KeyCode.Space)) { StartCoroutine(ScaleAnimation()); } } System.Collections.IEnumerator ScaleAnimation() { float elapsed 0f; Vector3 startScale Vector3.one; Vector3 endScale Vector3.one * 1.5f; // 放大到1.5倍 while (elapsed scaleDuration) { elapsed Time.deltaTime; float t elapsed / scaleDuration; // 使用缓动函数让动画更自然例如平滑的插值 t Mathf.SmoothStep(0f, 1f, t); Vector3 currentScale Vector3.Lerp(startScale, endScale, t); _scaler.SetTileScale(piecePosition, currentScale); yield return null; } // 动画结束后可以再缩放回来或者保持状态 // _scaler.SetTileScale(piecePosition, Vector3.one); } }6. 常见问题、优化与扩展在实际使用中你可能会遇到一些问题这里我总结了一些坑和对应的解决方案。6.1 常见问题排查问题现象可能原因解决方案缩放后Tile位置偏移不在网格中心缩放矩阵的中心点不对默认以世界原点缩放。确保使用CreateScaleMatrixAtCell方法它计算了以单元格中心为原点的正确矩阵。缩放没有立即生效修改缩放数据后没有调用_tilemap.RefreshTile(position)。在SetTileScale和ResetTileScale方法中务必调用RefreshTile。Tile闪烁或缩放值跳动每帧重置和设置矩阵的顺序或逻辑有误可能与其他修改Tilemap的脚本冲突。确保LateUpdate中的逻辑是先清理旧状态再设置新状态。检查是否有其他系统也在修改Tilemap的变换矩阵。性能下降尤其是动态Tile很多时每帧遍历所有动态Tile并计算矩阵CPU开销大。1.脏标记系统只在缩放值改变时更新对应Tile的矩阵而不是每帧全部更新。在SetTileScale中计算并存储矩阵LateUpdate只更新“脏”的Tile。2.分帧更新如果动态Tile数量巨大100可以考虑分帧更新每帧只处理一部分。缩放后Tile的碰撞体Tilemap Collider 2D没有跟着变大SetTransformMatrix只影响视觉渲染不影响物理碰撞体的几何形状。这是预期行为。如果你需要碰撞体同步缩放这是一个复杂得多的需求。可能需要1. 不使用Tilemap Collider而是为每个动态Tile单独生成并管理一个PolygonCollider2D。2. 或者通过修改CompositeCollider2D的生成数据非常复杂不推荐。通常游戏逻辑中判断单位位置是基于网格坐标的视觉缩放不影响逻辑。6.2 性能优化建议脏标记系统Dirty Flag这是最重要的优化。在DynamicTileScaler中增加一个HashSetVector3Int _dirtyPositions。当SetTileScale被调用时将位置加入这个集合。在LateUpdate中只遍历和处理_dirtyPositions中的Tile处理完后清空集合。对于缩放值不变的Tile完全跳过矩阵计算和设置。矩阵缓存在SetTileScale方法中如果新的缩放值和旧的相同可以直接跳过不标记为脏也不刷新Tile。避免在Update中频繁转换坐标如果你的控制逻辑需要根据世界坐标如鼠标点击来设置缩放确保只在事件触发时如OnMouseDown调用WorldToCell而不是每帧都调用。6.3 功能扩展思路这个基于Matrix4x4的方案是一个强大的基础你可以轻松地扩展它旋转Rotation在TileData中增加一个float rotation角度字段。在CreateScaleMatrixAtCell方法中将缩放矩阵Matrix4x4.Scale(scale)替换为复合变换Matrix4x4.Rotate(Quaternion.Euler(0,0,rotation)) * Matrix4x4.Scale(scale)。注意矩阵乘法的顺序先缩放后旋转还是先旋转后缩放会影响最终效果需要根据你的美术需求来定。自定义轴心点Pivot现在的缩放是以单元格中心为轴心。你可以修改CreateScaleMatrixAtCell接受一个额外的Vector2 pivot参数0到1之间表示在单元格内的相对位置来计算不同的轴心世界坐标。动画系统集成如上文的示例你可以很容易地将缩放值与Unity的Animation系统或DOTween等补间动画插件结合制作出平滑的选中、升级、受击动画效果。与Shader结合如果你需要更复杂的视觉效果如扭曲、波浪可以传递一个自定义的MaterialPropertyBlock给TilemapRenderer并在Shader中使用这个矩阵以及其他自定义属性。但这需要编写自定义Shader复杂度更高。这套方案我已在几个小型战棋和策略地图编辑项目中实际应用稳定性很不错。它成功地将Tilemap的批量渲染效率与单个元素的动态表现结合了起来避免了“为动态元素单独管理Sprite”带来的排序和逻辑复杂度。最大的心得就是理解Tilemap渲染的数据流是关键GetTileData、RefreshTile和SetTransformMatrix这几个API的配合使用是解锁高级Tilemap功能的钥匙。

本月热点