using UnityEngine; using System.Collections; using System.Linq; using System; using System.Collections.Generic; using UnityEngine.Serialization; namespace HexTiles { [SelectionBase] [AddComponentMenu("Ellyality/Visual/Hex/Tile Map")] public class HexTileMap : MonoBehaviour { [FormerlySerializedAs("hexWidth")] public float tileDiameter = 1f; [SerializeField] private bool drawHexPositionGizmos = false; [SerializeField] private int chunkSize = 10; public int ChunkSize { get { return chunkSize; } set { if (value < 1) { throw new ArgumentOutOfRangeException("value", "ChunkSize must be at least 1"); } chunkSize = value; } } /// /// Whether or not to draw the position of each tile on top of the tile in the editor. /// public bool DrawHexPositionHandles { get { return drawHexPositionGizmos; } set { drawHexPositionGizmos = value; } } [SerializeField] private bool drawWireframeWhenSelected = true; /// /// Whether or not to highlight all hexes in wireframe when the tile map is selected. /// public bool DrawWireframeWhenSelected { get { return drawWireframeWhenSelected; } set { drawWireframeWhenSelected = value; } } public enum HexCoordinateFormat { Axial, OffsetOddQ, WorldSpacePosition } /// /// The format to use when drawing hex position handles. /// public HexCoordinateFormat HexPositionHandleFormat { get; set; } /// /// Collection of all hex tiles that are part of this map. /// private IEnumerable Tiles { get { return Chunks .SelectMany(chunk => chunk .Tiles .Select(tile => new HexTileData(tile, chunk.TileDiameter, chunk.Material)) ); } } private IList chunks; private IList Chunks { get { if (chunks == null) { chunks = new List(); foreach (var chunk in GetComponentsInChildren()) { chunks.Add(chunk); } } return chunks; } } /// /// The current material used for painting tiles. Serialised here so that it will be saved for convenience /// when we have to reload scripts. /// public Material CurrentMaterial { get { return currentMaterial; } set { currentMaterial = value; } } [SerializeField] private Material currentMaterial; /// /// Highlighted tile for editing /// public HexCoords SelectedTile { get { return selectedTile; } set { selectedTile = value; } } private HexCoords selectedTile; /// /// Tile that the mouse is currently hovering over. /// public IEnumerable HighlightedTiles { get; set; } /// /// The position where a new tile will appear when we paint /// public IEnumerable NextTilePositions { get; set; } void OnDrawGizmosSelected() { if (HighlightedTiles != null) { foreach (var tile in HighlightedTiles) { DrawHexGizmo(HexPositionToWorldPosition(tile), Color.white); } } if (NextTilePositions != null) { foreach (var tile in NextTilePositions) { DrawHexGizmo(HexPositionToWorldPosition(tile), Color.cyan); } } if (SelectedTile != null) { var tile = FindTileForCoords(SelectedTile); if (tile != null) { DrawHexGizmo(HexPositionToWorldPosition(tile.Position), Color.green); } } } /// /// Returns the tile at the specified coordinates, or null /// if none exists. /// private HexTileData FindTileForCoords(HexCoords coords) { return Tiles .Where(t => t.Position.Coordinates == coords) .FirstOrDefault(); } /// /// Draws the outline of a hex at the specified position. /// Can be grey or green depending on whether it's highlighted or not. /// private void DrawHexGizmo(Vector3 position, Color color) { Gizmos.color = color; var verts = HexMetrics.GetHexVertices(tileDiameter) .Select(v => (transform.localToWorldMatrix * v) + (Vector4)position) .ToArray(); for (var i = 0; i < verts.Length; i++) { Gizmos.DrawLine(verts[i], verts[(i + 1) % verts.Length]); } } /// /// Take a vector in world space and return the closest hex coords. /// public HexCoords QuantizeVector3ToHexCoords(Vector3 vIn) { var vector = transform.InverseTransformPoint(vIn); var q = vector.x * 2f/3f / (tileDiameter/2f); var r = (-vector.x / 3f + Mathf.Sqrt(3f)/3f * vector.z) / (tileDiameter/2f); return new HexCoords (Mathf.RoundToInt(q), Mathf.RoundToInt(r)); } /// /// Get the world space position of the specified hex coords. /// This uses axial coordinates for the hexes. /// public Vector3 HexPositionToWorldPosition(HexPosition position) { return transform.TransformPoint(position.GetPositionVector(tileDiameter)); } /// /// Returns the nearest hex tile position in world space /// to the specified position. /// public Vector3 QuantizePositionToHexGrid(Vector3 vIn) { return HexPositionToWorldPosition(new HexPosition(QuantizeVector3ToHexCoords(vIn), vIn.y)); } /// /// Re-position and re-generate geometry for all tiles. /// Needed after changing global settings that affect all the tiles /// such as the tile size. /// /// Returns the chunks that were changed as a result of this operation. /// public IEnumerable RegenerateAllTiles() { var tileData = new List(); foreach (var hexTile in Tiles) { tileData.Add(hexTile); } Chunks.Clear(); var childChunks = GetComponentsInChildren(); foreach (var chunk in childChunks) { if (Application.isEditor) { DestroyImmediate(chunk.gameObject); } else { Destroy(chunk.gameObject); } } var modifiedChunks = tileData .Select(tile => CreateAndAddTile(tile.Position, tile.Material)) .Distinct() .ToArray(); // Need to force this to evaluate now so that CreateAndAddTile actually gets called. UpdateTileChunks(); return modifiedChunks; } /// /// Refresh the meshes of any chunks that have been modified. /// Returns true if any changes have been made. /// public bool UpdateTileChunks() { var updatedChunk = false; var oldChunks = Chunks.ToArray(); for (int i = 0; i < oldChunks.Length; i++) { // Remove the chunk if it has no tiles in it. if (oldChunks[i].Tiles.Count <= 0) { Chunks.Remove(oldChunks[i]); Utils.Destroy(oldChunks[i].gameObject); updatedChunk = true; continue; } // Re-generate meshes. if (oldChunks[i].Dirty) { oldChunks[i].GenerateMesh(); updatedChunk = true; } } return updatedChunk; } //public HexChunk FindChunkForCoordinates /// /// Add a tile to the map. Returns the chunk object containing the new tile. /// public ModifiedTileInfo CreateAndAddTile(HexPosition position, Material material) { var coords = position.Coordinates; var elevation = position.Elevation; var chunk = FindChunkForCoordinatesAndMaterial(coords, material); var chunkOperation = ModifiedTileInfo.ChunkOperation.Modified; // Create new chunk if necessary if (chunk == null) { chunk = CreateChunkForCoordinates(position.Coordinates, material); chunkOperation = ModifiedTileInfo.ChunkOperation.Added; } // See if there's already a tile at the specified position. var tile = FindTileForCoords(coords); if (tile != null) { // If a tlie at that position and that height already exists, return it. if (tile.Position.Elevation == elevation && tile.Material == material) { return new ModifiedTileInfo(chunk, chunkOperation); } // Remove the tile before adding a new one. TryRemovingTile(coords); } chunk.AddTile(position); // Generate side pieces // Note that we also need to update all the tiles adjacent to this one so that any side pieces that could be // Obscured by this one are removed. foreach (var side in HexMetrics.AdjacentHexes) { var adjacentTilePos = coords + side; var adjacentTile = FindTileForCoords(adjacentTilePos); if (adjacentTile != null) { var adjacentTileChunk = FindChunkForCoordinatesAndMaterial(adjacentTilePos, adjacentTile.Material); SetUpSidePiecesForTile(adjacentTilePos, adjacentTileChunk); } } SetUpSidePiecesForTile(coords, chunk); return new ModifiedTileInfo(chunk, chunkOperation); } /// /// Add a new chunk with the specified material and bounds around the specified coordinates. /// private HexChunk CreateChunkForCoordinates(HexCoords coordinates, Material material) { var lowerBounds = new HexCoords(RoundDownToInterval(coordinates.Q, chunkSize), RoundDownToInterval(coordinates.R, chunkSize)); var upperBounds = new HexCoords(lowerBounds.Q + chunkSize, lowerBounds.R + chunkSize); return CreateNewChunk(lowerBounds, upperBounds, material); } /// /// Returns the chunk for the tile at the specified coordinates, or /// null if none exists. /// public HexChunk FindChunkForCoordinates(HexCoords coordinates) { return Chunks.Where(c => c.Tiles.Where(tile => tile.Coordinates == coordinates).Any()) .FirstOrDefault(); } /// /// Find a chunk with bounds that match the specified coordinates, and the specified material. /// Returns null if none was found. /// public HexChunk FindChunkForCoordinatesAndMaterial(HexCoords coordinates, Material material) { // Try to find existing chunk. var matchingChunks = Chunks.Where(c => coordinates.IsWithinBounds(c.lowerBounds, c.upperBounds)) .Where(c => c.Material == material); if (matchingChunks.Count() > 1) { Debug.LogWarning("Overlapping chunks detected for coordinates " + coordinates + ". Taking first."); } return matchingChunks.FirstOrDefault(); } /// /// Create a new chunk with the specified bounds and material. /// private HexChunk CreateNewChunk(HexCoords lowerBounds, HexCoords upperBounds, Material material) { var newGameObject = new GameObject(string.Format("Chunk {0} - {1}", lowerBounds, upperBounds)); newGameObject.transform.parent = transform; var hexChunk = newGameObject.AddComponent(); hexChunk.lowerBounds = lowerBounds; hexChunk.upperBounds = upperBounds; hexChunk.Material = material; hexChunk.TileDiameter = tileDiameter; Chunks.Add(hexChunk); return hexChunk; } /// /// Round the input integer down to the nearest multiple of the supplied interval. /// Used to calculate which chunk a tile falls inside the bounds of. /// private static int RoundDownToInterval(int input, int interval) { return ((int)Math.Floor(input / (float)interval)) * interval; } private void SetUpSidePiecesForTile(HexCoords position, HexChunk tileChunk) { var tile = FindTileForCoords(position); if (tile == null) { throw new ApplicationException("Tried to set up side pieces for non-existent tile."); } foreach (var side in HexMetrics.AdjacentHexes) { var sidePosition = position + side; var adjacentTile = FindTileForCoords(sidePosition); if (adjacentTile != null) { var chunkWithTile = FindChunkForCoordinatesAndMaterial(sidePosition, adjacentTile.Material); if (chunkWithTile != null) { chunkWithTile.TryRemovingSidePiece(position, side); if (adjacentTile.Position.Elevation < tile.Position.Elevation) { tileChunk.AddSidePiece(position, side, tile.Position.Elevation - adjacentTile.Position.Elevation); } } } } } /// /// Attempt to remove the tile at the specified position. /// Returns true if it was removed successfully, false if no tile was found at that position. /// public bool TryRemovingTile(HexCoords position) { var tile = FindTileForCoords(position); if (tile == null) { return false; } var chunksWithTile = Chunks.Where(c => c.Tiles.Select(pos => pos.Coordinates).Contains(position)); if (chunksWithTile == null || chunksWithTile.Count() < 1) { Debug.LogError("Tile found in internal tile collection but not in scene. Removing", this); } foreach (var chunk in chunksWithTile) { chunk.RemoveTile(position); } return true; } private GameObject SpawnTileObject(HexPosition position) { var newObject = new GameObject("Tile [" + position.Coordinates.Q + ", " + position.Coordinates.R + "]"); newObject.transform.parent = transform; newObject.transform.position = HexPositionToWorldPosition(position); return newObject; } /// /// Destroy all child tiles and clear hashtable. /// public void ClearAllTiles() { Chunks.Clear(); // Note that we must add all children to a list first because if we // destroy children as we loop through them, the array we're looping // through will change and we can miss some. var children = new List(); foreach (Transform child in transform) { children.Add(child.gameObject); } children.ForEach(child => DestroyImmediate(child)); } /// /// Returns whether or not the specified tile exists. /// public bool ContainsTile(HexCoords tileCoords) { return FindTileForCoords(tileCoords) != null; } /// /// Return data about the tile at the specified position. /// public bool TryGetTile(HexCoords tileCoords, out HexTileData data) { data = FindTileForCoords(tileCoords); return data != null; } /// /// Remove the tile at the specified coordinates and replace it with one with the specified material. /// Returns the chunk with the tile that was modified. /// public ModifiedTileInfo ReplaceMaterialOnTile(HexCoords tileCoords, Material material) { HexTileData tile; if (!TryGetTile(tileCoords, out tile)) { throw new ArgumentOutOfRangeException("Tried replacing material on tile but map contains no tile at position " + tileCoords); } // Early out if the material is the same. if (tile.Material == material) { var chunk = FindChunkForCoordinatesAndMaterial(tileCoords, material); return new ModifiedTileInfo(chunk, ModifiedTileInfo.ChunkOperation.Modified); } TryRemovingTile(tileCoords); return CreateAndAddTile(tile.Position, material); } /// /// Returns all the tiles in this map. /// public IEnumerable GetAllTiles() { return Tiles; } /// /// Clears the internal cache of chunks, which will be lazily rebuilt next /// time it's needed by scanning the scene hierarchy. Needed to be done after /// a chunk is deleted by an undo action. /// public void ClearChunkCache() { chunks = null; } } }