using System; using System.Linq; using System.Reflection; using System.Collections.Generic; using UnityEngine.Tilemaps; using UnityEngine.Serialization; namespace UnityEngine { /// /// Generic visual tile for creating different tilesets like terrain, pipeline, random or animated tiles. /// This is templated to accept a Neighbor Rule Class for Custom Rules. /// /// Neighbor Rule Class for Custom Rules public class RuleTile : RuleTile { /// /// Returns the Neighbor Rule Class type for this Rule Tile. /// public sealed override Type m_NeighborType => typeof(T); } /// /// Generic visual tile for creating different tilesets like terrain, pipeline, random or animated tiles. /// [Serializable] [HelpURL("https://docs.unity3d.com/Packages/com.unity.2d.tilemap.extras@latest/index.html?subfolder=/manual/RuleTile.html")] public class RuleTile : TileBase { /// /// Returns the default Neighbor Rule Class type. /// public virtual Type m_NeighborType => typeof(TilingRuleOutput.Neighbor); /// /// The Default Sprite set when creating a new Rule. /// public Sprite m_DefaultSprite; /// /// The Default GameObject set when creating a new Rule. /// public GameObject m_DefaultGameObject; /// /// The Default Collider Type set when creating a new Rule. /// public Tile.ColliderType m_DefaultColliderType = Tile.ColliderType.Sprite; /// /// Angle in which the RuleTile is rotated by for matching in Degrees. /// public virtual int m_RotationAngle => 90; /// /// Number of rotations the RuleTile can be rotated by for matching. /// public int m_RotationCount => 360 / m_RotationAngle; /// /// The data structure holding the Rule information for matching Rule Tiles with /// its neighbors. /// [Serializable] public class TilingRuleOutput { /// /// Id for this Rule. /// public int m_Id; /// /// The output Sprites for this Rule. /// public Sprite[] m_Sprites = new Sprite[1]; /// /// The output GameObject for this Rule. /// public GameObject m_GameObject; /// /// The output minimum Animation Speed for this Rule. /// [FormerlySerializedAs("m_AnimationSpeed")] public float m_MinAnimationSpeed = 1f; /// /// The output maximum Animation Speed for this Rule. /// [FormerlySerializedAs("m_AnimationSpeed")] public float m_MaxAnimationSpeed = 1f; /// /// The perlin scale factor for this Rule. /// public float m_PerlinScale = 0.5f; /// /// The output type for this Rule. /// public OutputSprite m_Output = OutputSprite.Single; /// /// The output Collider Type for this Rule. /// public Tile.ColliderType m_ColliderType = Tile.ColliderType.Sprite; /// /// The randomized transform output for this Rule. /// public Transform m_RandomTransform; /// /// The enumeration for matching Neighbors when matching Rule Tiles /// public class Neighbor { /// /// The Rule Tile will check if the contents of the cell in that direction is an instance of this Rule Tile. /// If not, the rule will fail. /// public const int This = 1; /// /// The Rule Tile will check if the contents of the cell in that direction is not an instance of this Rule Tile. /// If it is, the rule will fail. /// public const int NotThis = 2; } /// /// The enumeration for the transform rule used when matching Rule Tiles. /// public enum Transform { /// /// The Rule Tile will match Tiles exactly as laid out in its neighbors. /// Fixed, /// /// The Rule Tile will rotate and match its neighbors. /// Rotated, /// /// The Rule Tile will mirror in the X axis and match its neighbors. /// MirrorX, /// /// The Rule Tile will mirror in the Y axis and match its neighbors. /// MirrorY, /// /// The Rule Tile will mirror in the X or Y axis and match its neighbors. /// MirrorXY } /// /// The Output for the Tile which fits this Rule. /// public enum OutputSprite { /// /// A Single Sprite will be output. /// Single, /// /// A Random Sprite will be output. /// Random, /// /// A Sprite Animation will be output. /// Animation } } /// /// The data structure holding the Rule information for matching Rule Tiles with /// its neighbors. /// [Serializable] public class TilingRule : TilingRuleOutput { /// /// The matching Rule conditions for each of its neighboring Tiles. /// public List m_Neighbors = new List(); /// /// * Preset this list to RuleTile backward compatible, but not support for HexagonalRuleTile backward compatible. /// public List m_NeighborPositions = new List() { new Vector3Int(-1, 1, 0), new Vector3Int(0, 1, 0), new Vector3Int(1, 1, 0), new Vector3Int(-1, 0, 0), new Vector3Int(1, 0, 0), new Vector3Int(-1, -1, 0), new Vector3Int(0, -1, 0), new Vector3Int(1, -1, 0), }; /// /// The transform matching Rule for this Rule. /// public Transform m_RuleTransform; /// /// This clones a copy of the TilingRule. /// /// A copy of the TilingRule. public TilingRule Clone() { TilingRule rule = new TilingRule { m_Neighbors = new List(m_Neighbors), m_NeighborPositions = new List(m_NeighborPositions), m_RuleTransform = m_RuleTransform, m_Sprites = new Sprite[m_Sprites.Length], m_GameObject = m_GameObject, m_MinAnimationSpeed = m_MinAnimationSpeed, m_MaxAnimationSpeed = m_MaxAnimationSpeed, m_PerlinScale = m_PerlinScale, m_Output = m_Output, m_ColliderType = m_ColliderType, m_RandomTransform = m_RandomTransform, }; Array.Copy(m_Sprites, rule.m_Sprites, m_Sprites.Length); return rule; } /// /// Returns all neighbors of this Tile as a dictionary /// /// A dictionary of neighbors for this Tile public Dictionary GetNeighbors() { Dictionary dict = new Dictionary(); for (int i = 0; i < m_Neighbors.Count && i < m_NeighborPositions.Count; i++) dict.Add(m_NeighborPositions[i], m_Neighbors[i]); return dict; } /// /// Applies the values from the given dictionary as this Tile's neighbors /// /// Dictionary to apply values from public void ApplyNeighbors(Dictionary dict) { m_NeighborPositions = dict.Keys.ToList(); m_Neighbors = dict.Values.ToList(); } /// /// Gets the cell bounds of the TilingRule. /// /// Returns the cell bounds of the TilingRule. public BoundsInt GetBounds() { BoundsInt bounds = new BoundsInt(Vector3Int.zero, Vector3Int.one); foreach (var neighbor in GetNeighbors()) { bounds.xMin = Mathf.Min(bounds.xMin, neighbor.Key.x); bounds.yMin = Mathf.Min(bounds.yMin, neighbor.Key.y); bounds.xMax = Mathf.Max(bounds.xMax, neighbor.Key.x + 1); bounds.yMax = Mathf.Max(bounds.yMax, neighbor.Key.y + 1); } return bounds; } } /// /// Attribute which marks a property which cannot be overridden by a RuleOverrideTile /// public class DontOverride : Attribute { } /// /// A list of Tiling Rules for the Rule Tile. /// [HideInInspector] public List m_TilingRules = new List(); /// /// Returns a set of neighboring positions for this RuleTile /// public HashSet neighborPositions { get { if (m_NeighborPositions.Count == 0) UpdateNeighborPositions(); return m_NeighborPositions; } } private HashSet m_NeighborPositions = new HashSet(); /// /// Updates the neighboring positions of this RuleTile /// public void UpdateNeighborPositions() { m_CacheTilemapsNeighborPositions.Clear(); HashSet positions = m_NeighborPositions; positions.Clear(); foreach (TilingRule rule in m_TilingRules) { foreach (var neighbor in rule.GetNeighbors()) { Vector3Int position = neighbor.Key; positions.Add(position); // Check rule against rotations of 0, 90, 180, 270 if (rule.m_RuleTransform == TilingRuleOutput.Transform.Rotated) { for (int angle = m_RotationAngle; angle < 360; angle += m_RotationAngle) { positions.Add(GetRotatedPosition(position, angle)); } } // Check rule against x-axis, y-axis mirror else if (rule.m_RuleTransform == TilingRuleOutput.Transform.MirrorXY) { positions.Add(GetMirroredPosition(position, true, true)); positions.Add(GetMirroredPosition(position, true, false)); positions.Add(GetMirroredPosition(position, false, true)); } // Check rule against x-axis mirror else if (rule.m_RuleTransform == TilingRuleOutput.Transform.MirrorX) { positions.Add(GetMirroredPosition(position, true, false)); } // Check rule against y-axis mirror else if (rule.m_RuleTransform == TilingRuleOutput.Transform.MirrorY) { positions.Add(GetMirroredPosition(position, false, true)); } } } } /// /// StartUp is called on the first frame of the running Scene. /// /// Position of the Tile on the Tilemap. /// The Tilemap the tile is present on. /// The GameObject instantiated for the Tile. /// Whether StartUp was successful public override bool StartUp(Vector3Int position, ITilemap tilemap, GameObject instantiatedGameObject) { if (instantiatedGameObject != null) { Tilemap tmpMap = tilemap.GetComponent(); Matrix4x4 orientMatrix = tmpMap.orientationMatrix; var iden = Matrix4x4.identity; Vector3 gameObjectTranslation = new Vector3(); Quaternion gameObjectRotation = new Quaternion(); Vector3 gameObjectScale = new Vector3(); bool ruleMatched = false; Matrix4x4 transform = iden; foreach (TilingRule rule in m_TilingRules) { if (RuleMatches(rule, position, tilemap, ref transform)) { transform = orientMatrix * transform; // Converts the tile's translation, rotation, & scale matrix to values to be used by the instantiated GameObject gameObjectTranslation = new Vector3(transform.m03, transform.m13, transform.m23); gameObjectRotation = Quaternion.LookRotation(new Vector3(transform.m02, transform.m12, transform.m22), new Vector3(transform.m01, transform.m11, transform.m21)); gameObjectScale = transform.lossyScale; ruleMatched = true; break; } } if (!ruleMatched) { // Fallback to just using the orientMatrix for the translation, rotation, & scale values. gameObjectTranslation = new Vector3(orientMatrix.m03, orientMatrix.m13, orientMatrix.m23); gameObjectRotation = Quaternion.LookRotation(new Vector3(orientMatrix.m02, orientMatrix.m12, orientMatrix.m22), new Vector3(orientMatrix.m01, orientMatrix.m11, orientMatrix.m21)); gameObjectScale = orientMatrix.lossyScale; } instantiatedGameObject.transform.localPosition = gameObjectTranslation + tmpMap.CellToLocalInterpolated(position + tmpMap.tileAnchor); instantiatedGameObject.transform.localRotation = gameObjectRotation; instantiatedGameObject.transform.localScale = gameObjectScale; } return true; } /// /// Retrieves any tile rendering data from the scripted tile. /// /// Position of the Tile on the Tilemap. /// The Tilemap the tile is present on. /// Data to render the tile. public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData) { var iden = Matrix4x4.identity; tileData.sprite = m_DefaultSprite; tileData.gameObject = m_DefaultGameObject; tileData.colliderType = m_DefaultColliderType; tileData.flags = TileFlags.LockTransform; tileData.transform = iden; Matrix4x4 transform = iden; foreach (TilingRule rule in m_TilingRules) { if (RuleMatches(rule, position, tilemap, ref transform)) { switch (rule.m_Output) { case TilingRuleOutput.OutputSprite.Single: case TilingRuleOutput.OutputSprite.Animation: tileData.sprite = rule.m_Sprites[0]; break; case TilingRuleOutput.OutputSprite.Random: int index = Mathf.Clamp(Mathf.FloorToInt(GetPerlinValue(position, rule.m_PerlinScale, 100000f) * rule.m_Sprites.Length), 0, rule.m_Sprites.Length - 1); tileData.sprite = rule.m_Sprites[index]; if (rule.m_RandomTransform != TilingRuleOutput.Transform.Fixed) transform = ApplyRandomTransform(rule.m_RandomTransform, transform, rule.m_PerlinScale, position); break; } tileData.transform = transform; tileData.gameObject = rule.m_GameObject; tileData.colliderType = rule.m_ColliderType; break; } } } /// /// Returns a Perlin Noise value based on the given inputs. /// /// Position of the Tile on the Tilemap. /// The Perlin Scale factor of the Tile. /// Offset of the Tile on the Tilemap. /// A Perlin Noise value based on the given inputs. public static float GetPerlinValue(Vector3Int position, float scale, float offset) { return Mathf.PerlinNoise((position.x + offset) * scale, (position.y + offset) * scale); } static Dictionary, HashSet>> m_CacheTilemapsNeighborPositions = new Dictionary, HashSet>>(); static TileBase[] m_AllocatedUsedTileArr = Array.Empty(); static bool IsTilemapUsedTilesChange(Tilemap tilemap, out KeyValuePair, HashSet> hashSet) { if (!m_CacheTilemapsNeighborPositions.TryGetValue(tilemap, out hashSet)) return true; var oldUsedTiles = hashSet.Key; int newUsedTilesCount = tilemap.GetUsedTilesCount(); if (newUsedTilesCount != oldUsedTiles.Count) return true; if (m_AllocatedUsedTileArr.Length < newUsedTilesCount) Array.Resize(ref m_AllocatedUsedTileArr, newUsedTilesCount); tilemap.GetUsedTilesNonAlloc(m_AllocatedUsedTileArr); for (int i = 0; i < newUsedTilesCount; i++) { TileBase newUsedTile = m_AllocatedUsedTileArr[i]; if (!oldUsedTiles.Contains(newUsedTile)) return true; } return false; } static KeyValuePair, HashSet> CachingTilemapNeighborPositions(Tilemap tilemap) { int usedTileCount = tilemap.GetUsedTilesCount(); HashSet usedTiles = new HashSet(); HashSet neighborPositions = new HashSet(); if (m_AllocatedUsedTileArr.Length < usedTileCount) Array.Resize(ref m_AllocatedUsedTileArr, usedTileCount); tilemap.GetUsedTilesNonAlloc(m_AllocatedUsedTileArr); for (int i = 0; i < usedTileCount; i++) { TileBase tile = m_AllocatedUsedTileArr[i]; usedTiles.Add(tile); RuleTile ruleTile = null; if (tile is RuleTile rt) ruleTile = rt; else if (tile is RuleOverrideTile ot) ruleTile = ot.m_Tile; if (ruleTile) foreach (Vector3Int neighborPosition in ruleTile.neighborPositions) neighborPositions.Add(neighborPosition); } var value = new KeyValuePair, HashSet>(usedTiles, neighborPositions); m_CacheTilemapsNeighborPositions[tilemap] = value; return value; } static bool NeedRelease() { foreach (var keypair in m_CacheTilemapsNeighborPositions) { if (keypair.Key == null) { return true; } } return false; } static void ReleaseDestroyedTilemapCacheData() { if (!NeedRelease()) return; var hasCleared = false; var keys = m_CacheTilemapsNeighborPositions.Keys.ToArray(); foreach (var key in keys) { if (key == null && m_CacheTilemapsNeighborPositions.Remove(key)) hasCleared = true; } if (hasCleared) { // TrimExcess m_CacheTilemapsNeighborPositions = new Dictionary, HashSet>>(m_CacheTilemapsNeighborPositions); } } /// /// Retrieves any tile animation data from the scripted tile. /// /// Position of the Tile on the Tilemap. /// The Tilemap the tile is present on. /// Data to run an animation on the tile. /// Whether the call was successful. public override bool GetTileAnimationData(Vector3Int position, ITilemap tilemap, ref TileAnimationData tileAnimationData) { Matrix4x4 transform = Matrix4x4.identity; foreach (TilingRule rule in m_TilingRules) { if (rule.m_Output == TilingRuleOutput.OutputSprite.Animation) { if (RuleMatches(rule, position, tilemap, ref transform)) { tileAnimationData.animatedSprites = rule.m_Sprites; tileAnimationData.animationSpeed = Random.Range( rule.m_MinAnimationSpeed, rule.m_MaxAnimationSpeed); return true; } } } return false; } /// /// This method is called when the tile is refreshed. /// /// Position of the Tile on the Tilemap. /// The Tilemap the tile is present on. public override void RefreshTile(Vector3Int position, ITilemap tilemap) { base.RefreshTile(position, tilemap); Tilemap baseTilemap = tilemap.GetComponent(); ReleaseDestroyedTilemapCacheData(); // Prevent memory leak if (IsTilemapUsedTilesChange(baseTilemap, out var neighborPositionsSet)) neighborPositionsSet = CachingTilemapNeighborPositions(baseTilemap); var neighborPositionsRuleTile = neighborPositionsSet.Value; foreach (Vector3Int offset in neighborPositionsRuleTile) { Vector3Int offsetPosition = GetOffsetPositionReverse(position, offset); TileBase tile = tilemap.GetTile(offsetPosition); RuleTile ruleTile = null; if (tile is RuleTile rt) ruleTile = rt; else if (tile is RuleOverrideTile ot) ruleTile = ot.m_Tile; if (ruleTile != null) if (ruleTile == this || ruleTile.neighborPositions.Contains(offset)) base.RefreshTile(offsetPosition, tilemap); } } /// /// Does a Rule Match given a Tiling Rule and neighboring Tiles. /// /// The Tiling Rule to match with. /// Position of the Tile on the Tilemap. /// The tilemap to match with. /// A transform matrix which will match the Rule. /// True if there is a match, False if not. public virtual bool RuleMatches(TilingRule rule, Vector3Int position, ITilemap tilemap, ref Matrix4x4 transform) { if (RuleMatches(rule, position, tilemap, 0)) { transform = Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0f, 0f, 0f), Vector3.one); return true; } // Check rule against rotations of 0, 90, 180, 270 if (rule.m_RuleTransform == TilingRuleOutput.Transform.Rotated) { for (int angle = m_RotationAngle; angle < 360; angle += m_RotationAngle) { if (RuleMatches(rule, position, tilemap, angle)) { transform = Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0f, 0f, -angle), Vector3.one); return true; } } } // Check rule against x-axis, y-axis mirror else if (rule.m_RuleTransform == TilingRuleOutput.Transform.MirrorXY) { if (RuleMatches(rule, position, tilemap, true, true)) { transform = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(-1f, -1f, 1f)); return true; } if (RuleMatches(rule, position, tilemap, true, false)) { transform = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(-1f, 1f, 1f)); return true; } if (RuleMatches(rule, position, tilemap, false, true)) { transform = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1f, -1f, 1f)); return true; } } // Check rule against x-axis mirror else if (rule.m_RuleTransform == TilingRuleOutput.Transform.MirrorX) { if (RuleMatches(rule, position, tilemap, true, false)) { transform = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(-1f, 1f, 1f)); return true; } } // Check rule against y-axis mirror else if (rule.m_RuleTransform == TilingRuleOutput.Transform.MirrorY) { if (RuleMatches(rule, position, tilemap, false, true)) { transform = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1f, -1f, 1f)); return true; } } return false; } /// /// Returns a random transform matrix given the random transform rule. /// /// Random transform rule. /// The original transform matrix. /// The Perlin Scale factor of the Tile. /// Position of the Tile on the Tilemap. /// A random transform matrix. public virtual Matrix4x4 ApplyRandomTransform(TilingRuleOutput.Transform type, Matrix4x4 original, float perlinScale, Vector3Int position) { float perlin = GetPerlinValue(position, perlinScale, 200000f); switch (type) { case TilingRuleOutput.Transform.MirrorXY: return original * Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(Math.Abs(perlin - 0.5) > 0.25 ? 1f : -1f, perlin < 0.5 ? 1f : -1f, 1f)); case TilingRuleOutput.Transform.MirrorX: return original * Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(perlin < 0.5 ? 1f : -1f, 1f, 1f)); case TilingRuleOutput.Transform.MirrorY: return original * Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1f, perlin < 0.5 ? 1f : -1f, 1f)); case TilingRuleOutput.Transform.Rotated: int angle = Mathf.Clamp(Mathf.FloorToInt(perlin * m_RotationCount), 0, m_RotationCount - 1) * m_RotationAngle; return Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0f, 0f, -angle), Vector3.one); } return original; } /// /// Returns custom fields for this RuleTile /// /// Whether override fields are returned /// Custom fields for this RuleTile public FieldInfo[] GetCustomFields(bool isOverrideInstance) { return this.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) .Where(field => typeof(RuleTile).GetField(field.Name) == null) .Where(field => field.IsPublic || field.IsDefined(typeof(SerializeField))) .Where(field => !field.IsDefined(typeof(HideInInspector))) .Where(field => !isOverrideInstance || !field.IsDefined(typeof(DontOverride))) .ToArray(); } /// /// Checks if there is a match given the neighbor matching rule and a Tile. /// /// Neighbor matching rule. /// Tile to match. /// True if there is a match, False if not. public virtual bool RuleMatch(int neighbor, TileBase other) { if (other is RuleOverrideTile ot) other = ot.m_InstanceTile; switch (neighbor) { case TilingRuleOutput.Neighbor.This: return other == this; case TilingRuleOutput.Neighbor.NotThis: return other != this; } return true; } /// /// Checks if there is a match given the neighbor matching rule and a Tile with a rotation angle. /// /// Neighbor matching rule. /// Position of the Tile on the Tilemap. /// Tilemap to match. /// Rotation angle for matching. /// True if there is a match, False if not. public bool RuleMatches(TilingRule rule, Vector3Int position, ITilemap tilemap, int angle) { var minCount = Math.Min(rule.m_Neighbors.Count, rule.m_NeighborPositions.Count); for (int i = 0; i < minCount ; i++) { int neighbor = rule.m_Neighbors[i]; Vector3Int positionOffset = GetRotatedPosition(rule.m_NeighborPositions[i], angle); TileBase other = tilemap.GetTile(GetOffsetPosition(position, positionOffset)); if (!RuleMatch(neighbor, other)) { return false; } } return true; } /// /// Checks if there is a match given the neighbor matching rule and a Tile with mirrored axii. /// /// Neighbor matching rule. /// Position of the Tile on the Tilemap. /// Tilemap to match. /// Mirror X Axis for matching. /// Mirror Y Axis for matching. /// True if there is a match, False if not. public bool RuleMatches(TilingRule rule, Vector3Int position, ITilemap tilemap, bool mirrorX, bool mirrorY) { var minCount = Math.Min(rule.m_Neighbors.Count, rule.m_NeighborPositions.Count); for (int i = 0; i < minCount; i++) { int neighbor = rule.m_Neighbors[i]; Vector3Int positionOffset = GetMirroredPosition(rule.m_NeighborPositions[i], mirrorX, mirrorY); TileBase other = tilemap.GetTile(GetOffsetPosition(position, positionOffset)); if (!RuleMatch(neighbor, other)) { return false; } } return true; } /// /// Gets a rotated position given its original position and the rotation in degrees. /// /// Original position of Tile. /// Rotation in degrees. /// Rotated position of Tile. public virtual Vector3Int GetRotatedPosition(Vector3Int position, int rotation) { switch (rotation) { case 0: return position; case 90: return new Vector3Int(position.y, -position.x, 0); case 180: return new Vector3Int(-position.x, -position.y, 0); case 270: return new Vector3Int(-position.y, position.x, 0); } return position; } /// /// Gets a mirrored position given its original position and the mirroring axii. /// /// Original position of Tile. /// Mirror in the X Axis. /// Mirror in the Y Axis. /// Mirrored position of Tile. public virtual Vector3Int GetMirroredPosition(Vector3Int position, bool mirrorX, bool mirrorY) { if (mirrorX) position.x *= -1; if (mirrorY) position.y *= -1; return position; } /// /// Get the offset for the given position with the given offset. /// /// Position to offset. /// Offset for the position. /// The offset position. public virtual Vector3Int GetOffsetPosition(Vector3Int position, Vector3Int offset) { return position + offset; } /// /// Get the reversed offset for the given position with the given offset. /// /// Position to offset. /// Offset for the position. /// The reversed offset position. public virtual Vector3Int GetOffsetPositionReverse(Vector3Int position, Vector3Int offset) { return position - offset; } } }