551 lines
19 KiB
C#
551 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Godot;
|
|
using Godot.Collections;
|
|
|
|
public class World : Spatial
|
|
{
|
|
public enum GenerationState
|
|
{
|
|
Undefined,
|
|
Heightmap,
|
|
TileType,
|
|
Objects,
|
|
Done
|
|
}
|
|
|
|
// constants
|
|
public const int ChunkSize = 12;
|
|
public const int NumChunkRows = 3;
|
|
public const int NumChunkColumns = NumChunkRows;
|
|
private static readonly Color RockColor = new(0.5f, 0.5f, 0.4f);
|
|
private static readonly Color GrassColor = new(0, 0.4f, 0);
|
|
private static readonly Color DarkGrassColor = new(0.05882353f, 0.5411765f, 0.05882353f);
|
|
private static readonly Color LightWaterColor = new(0.05882353f, 0.05882353f, 0.8627451f);
|
|
|
|
private readonly Godot.Collections.Dictionary<Vector2, WorldChunk> _cachedWorldChunks;
|
|
private readonly List<Vector2> _addedChunkIndices = new();
|
|
private readonly List<WorldChunk> _unusedWorldChunks = new();
|
|
private readonly Image _heightmapImage = new();
|
|
private readonly List<Vector2> _removedChunkIndices = new();
|
|
private readonly Image _tileTypeMapImage = new();
|
|
private int FrameCounter;
|
|
|
|
// referenced scenes
|
|
private readonly PackedScene _worldChunkScene = GD.Load<PackedScene>("res://scenes/WorldChunk.tscn");
|
|
|
|
private List<Vector2> _activeChunkIndices = new();
|
|
private Rect2 _centerChunkRect;
|
|
|
|
// delegate void OnCoordClicked(Vector2 world_pos);
|
|
|
|
// other members
|
|
private Vector2 _centerPlaneCoord;
|
|
private Vector2 _chunkIndexNorthEast;
|
|
private Vector2 _chunkIndexSouthWest;
|
|
private Array<Spatial> _grassAssets;
|
|
private ImageTexture _heightmapTexture;
|
|
|
|
private OpenSimplexNoise _noiseGenerator = new();
|
|
private Array<Spatial> _rockAssets;
|
|
private MultiMeshInstance _tileMultiMeshInstance;
|
|
private int _usedTileMeshInstances;
|
|
private Array<Spatial> _treeAssets;
|
|
private ImageTexture _viewTileTypeTexture;
|
|
public Vector2 CenterChunkIndex = Vector2.Zero;
|
|
public Spatial Chunks;
|
|
public Color DebugColor;
|
|
public HexGrid HexGrid = new();
|
|
public int Seed = 0;
|
|
|
|
public GenerationState State = GenerationState.Done;
|
|
public Vector2 WorldTextureCoordinateOffset;
|
|
|
|
|
|
// ui elements
|
|
|
|
// scene nodes
|
|
|
|
// resources
|
|
|
|
// exports
|
|
// [Export] public Vector2 Size = new Vector2(1, 1);
|
|
|
|
// signals
|
|
[Signal]
|
|
private delegate void OnTilesChanged(Array<Vector2> removedChunkIndices, Array<Vector2> addedChunkIndices);
|
|
|
|
[Signal]
|
|
private delegate void OnWorldViewTileTypeImageChanged(Image viewTileTypeImage);
|
|
|
|
[Signal]
|
|
private delegate void OnHeightmapImageChanged(Image heightmapImage);
|
|
|
|
[Signal]
|
|
public delegate void EntityClicked(Entity entity);
|
|
|
|
// signals
|
|
[Signal]
|
|
private delegate void TileClicked(HexTile3D tile3d);
|
|
|
|
[Signal]
|
|
private delegate void TileHovered(HexTile3D tile3d);
|
|
|
|
public World()
|
|
{
|
|
Debug.Assert(ChunkSize % 2 == 0);
|
|
|
|
_cachedWorldChunks = new Godot.Collections.Dictionary<Vector2, WorldChunk>();
|
|
}
|
|
|
|
// Called when the node enters the scene tree for the first time.
|
|
public override void _Ready()
|
|
{
|
|
Chunks = (Spatial)FindNode("Chunks");
|
|
Debug.Assert(Chunks != null);
|
|
|
|
_tileMultiMeshInstance = (MultiMeshInstance)FindNode("TileMultiMeshInstance");
|
|
Debug.Assert(_tileMultiMeshInstance != null);
|
|
_tileMultiMeshInstance.Multimesh.InstanceCount =
|
|
ChunkSize * ChunkSize * NumChunkColumns * NumChunkRows;
|
|
_usedTileMeshInstances = 0;
|
|
|
|
InitNoiseGenerator();
|
|
|
|
GetNode<Spatial>("Assets").Visible = false;
|
|
|
|
_rockAssets = new Array<Spatial>();
|
|
foreach (Spatial asset in GetNode<Node>("Assets/Rocks").GetChildren()) _rockAssets.Add(asset);
|
|
|
|
_grassAssets = new Array<Spatial>();
|
|
foreach (Spatial asset in GetNode<Node>("Assets/Grass").GetChildren()) _grassAssets.Add(asset);
|
|
|
|
_treeAssets = new Array<Spatial>();
|
|
foreach (Spatial asset in GetNode<Node>("Assets/Trees").GetChildren()) _treeAssets.Add(asset);
|
|
|
|
SetCenterPlaneCoord(Vector2.Zero);
|
|
}
|
|
|
|
public void InitNoiseGenerator()
|
|
{
|
|
_noiseGenerator = new OpenSimplexNoise();
|
|
|
|
_noiseGenerator.Seed = Seed;
|
|
_noiseGenerator.Octaves = 1;
|
|
_noiseGenerator.Period = 10;
|
|
_noiseGenerator.Persistence = 0.5f;
|
|
_noiseGenerator.Lacunarity = 2;
|
|
}
|
|
|
|
public WorldChunk GetOrCreateWorldChunk(Vector2 chunkIndex, Color debugColor)
|
|
{
|
|
WorldChunk chunk;
|
|
|
|
if (IsChunkCached(chunkIndex))
|
|
return _cachedWorldChunks[chunkIndex];
|
|
|
|
if (_unusedWorldChunks.Count > 0)
|
|
{
|
|
chunk = _unusedWorldChunks.First();
|
|
_unusedWorldChunks.RemoveAt(0);
|
|
|
|
GD.Print("Reusing chunk from former index " + chunk.ChunkIndex + " at new index " + chunkIndex);
|
|
}
|
|
else
|
|
{
|
|
chunk = CreateWorldChunk(chunkIndex, debugColor);
|
|
}
|
|
|
|
_cachedWorldChunks[chunkIndex] = chunk;
|
|
|
|
return chunk;
|
|
}
|
|
|
|
private bool IsChunkCached(Vector2 chunkIndex)
|
|
{
|
|
return _cachedWorldChunks.ContainsKey(chunkIndex);
|
|
}
|
|
|
|
|
|
private WorldChunk CreateWorldChunk(Vector2 chunkIndex, Color debugColor)
|
|
{
|
|
WorldChunk result = (WorldChunk)_worldChunkScene.Instance();
|
|
Chunks.AddChild(result);
|
|
result.Connect("TileClicked", this, nameof(OnTileClicked));
|
|
result.Connect("TileHovered", this, nameof(OnTileHovered));
|
|
|
|
result.SetSize(ChunkSize);
|
|
result.InitializeTileInstances(chunkIndex, _tileMultiMeshInstance, _usedTileMeshInstances);
|
|
_usedTileMeshInstances += result.Tiles.GetChildCount();
|
|
|
|
result.SetChunkIndex(chunkIndex, HexGrid);
|
|
result.UpdateTileTransforms();
|
|
|
|
result.DebugColor = debugColor;
|
|
result.DebugColor.a = 0.6f;
|
|
|
|
_cachedWorldChunks.Add(chunkIndex, result);
|
|
|
|
return result;
|
|
}
|
|
|
|
private bool IsColorEqualApprox(Color colorA, Color colorB)
|
|
{
|
|
Vector3 colorDifference = new(colorA.r - colorB.r, colorA.g - colorB.g, colorA.b - colorB.b);
|
|
return colorDifference.LengthSquared() < 0.1 * 0.1;
|
|
}
|
|
|
|
private Spatial SelectAsset(Vector2 textureCoord, Array<Spatial> assets, Random randomGenerator, double probability)
|
|
{
|
|
if (randomGenerator.NextDouble() < 1.0 - probability) return null;
|
|
|
|
int assetIndex = randomGenerator.Next(assets.Count);
|
|
Spatial assetInstance = (Spatial)assets[assetIndex].Duplicate();
|
|
Transform assetTransform = Transform.Identity;
|
|
assetTransform.origin = HexGrid.GetHexCenterVec3FromOffset(textureCoord);
|
|
// TODO: assetTransform.origin.y = GetHeightAtOffset(offsetCoord);
|
|
assetTransform.origin.y = 0;
|
|
assetTransform.basis =
|
|
assetTransform.basis.Rotated(Vector3.Up, (float)(randomGenerator.NextDouble() * Mathf.Pi * 2));
|
|
assetInstance.Transform = assetTransform;
|
|
|
|
return assetInstance;
|
|
}
|
|
|
|
private void PopulateChunk(WorldChunk chunk)
|
|
{
|
|
Random environmentRandom = new(Seed);
|
|
|
|
Image tileTypeImage = chunk.TileTypeOffscreenViewport.GetTexture().GetData();
|
|
tileTypeImage.Lock();
|
|
|
|
foreach (int textureCoordU in Enumerable.Range(0, chunk.Size))
|
|
foreach (int textureCoordV in Enumerable.Range(0, chunk.Size))
|
|
{
|
|
Color colorValue = tileTypeImage.GetPixel(textureCoordU, textureCoordV);
|
|
Vector2 textureCoord = new(textureCoordU, textureCoordV);
|
|
Vector2 offsetCoord = chunk.ChunkIndex * ChunkSize + textureCoord;
|
|
|
|
if (IsColorEqualApprox(colorValue, RockColor))
|
|
{
|
|
Spatial rockAsset = SelectAsset(textureCoord, _rockAssets, environmentRandom, 0.15);
|
|
if (rockAsset != null) chunk.Entities.AddChild(rockAsset);
|
|
// TODO: MarkCellUnwalkable(cell);
|
|
}
|
|
else if (IsColorEqualApprox(colorValue, GrassColor) || IsColorEqualApprox(colorValue, DarkGrassColor))
|
|
{
|
|
Spatial grassAsset = SelectAsset(textureCoord, _grassAssets, environmentRandom, 0.15);
|
|
if (grassAsset != null) chunk.Entities.AddChild(grassAsset);
|
|
|
|
Tree treeAsset = SelectAsset(textureCoord, _treeAssets, environmentRandom, 0.05) as Tree;
|
|
if (treeAsset != null)
|
|
{
|
|
chunk.Entities.AddChild(treeAsset);
|
|
treeAsset.Connect("EntityClicked", this, nameof(OnEntityClicked));
|
|
}
|
|
|
|
|
|
// TODO: MarkCellUnwalkable(cell);
|
|
// else if (environmentRandom.NextDouble() < 0.01)
|
|
// {
|
|
// var chestAsset = (Chest)_chestScene.Instance();
|
|
// var assetTransform = Transform.Identity;
|
|
// assetTransform.origin = GetHexCenterFromOffset(offsetCoord);
|
|
// assetTransform.origin.y = GetHeightAtOffset(offsetCoord);
|
|
// chestAsset.Transform = assetTransform;
|
|
// Entities.AddChild(chestAsset);
|
|
// MarkCellUnwalkable(cell);
|
|
// }
|
|
}
|
|
// else if (IsColorWater(colorValue))
|
|
// {
|
|
// MarkCellUnwalkable(cell);
|
|
// }
|
|
}
|
|
|
|
tileTypeImage.Unlock();
|
|
}
|
|
|
|
public Vector2 WorldToOffsetCoords(Vector3 fromPositionWorld)
|
|
{
|
|
return HexGrid.GetHexAt(new Vector2(fromPositionWorld.x, fromPositionWorld.z)).OffsetCoords;
|
|
}
|
|
|
|
public Vector3 GetHexCenterFromOffset(Vector2 fromPositionOffset)
|
|
{
|
|
return HexGrid.GetHexCenterVec3FromOffset(fromPositionOffset);
|
|
}
|
|
|
|
public void UpdateCenterChunkFromPlaneCoord(Vector2 planeCoord)
|
|
{
|
|
if (State != GenerationState.Done)
|
|
{
|
|
GD.PrintErr("Cannot update chunk to new planeCoord " + planeCoord + ": Chunk generation not yet finished!");
|
|
return;
|
|
}
|
|
|
|
GD.Print("Update Chunks: " + FrameCounter);
|
|
|
|
// mark all chunks as retired
|
|
Godot.Collections.Dictionary<Vector2, WorldChunk> oldCachedChunks = new(_cachedWorldChunks);
|
|
|
|
// set new center chunk
|
|
CenterChunkIndex = GetChunkTupleFromPlaneCoord(planeCoord);
|
|
|
|
WorldChunk currentChunk = GetOrCreateWorldChunk(CenterChunkIndex,
|
|
new Color(GD.Randf(), GD.Randf(), GD.Randf()));
|
|
|
|
|
|
_centerChunkRect = new Rect2(
|
|
new Vector2(currentChunk.Transform.origin.x, currentChunk.Transform.origin.z)
|
|
+ currentChunk.PlaneRect.Position,
|
|
currentChunk.PlaneRect.Size);
|
|
GD.Print("Center Chunk Rect: " + _centerChunkRect.Position + " size: " + _centerChunkRect.Size);
|
|
|
|
// load or create adjacent chunks
|
|
_activeChunkIndices = new List<Vector2>();
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(-1, -1));
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(0, -1));
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(1, -1));
|
|
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(-1, 0));
|
|
_activeChunkIndices.Add(CenterChunkIndex);
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(+1, 0));
|
|
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(-1, +1));
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(0, +1));
|
|
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(+1, +1));
|
|
|
|
// clear unused chunks
|
|
_unusedWorldChunks.Clear();
|
|
_addedChunkIndices.Clear();
|
|
|
|
foreach (Vector2 oldChunkIndex in oldCachedChunks.Keys)
|
|
if (!_activeChunkIndices.Contains(oldChunkIndex))
|
|
DeactivateChunk(oldCachedChunks[oldChunkIndex]);
|
|
|
|
foreach (Vector2 activeChunkIndex in _activeChunkIndices)
|
|
{
|
|
WorldChunk chunk = GetOrCreateWorldChunk(activeChunkIndex,
|
|
new Color(GD.Randf(), GD.Randf(), GD.Randf()));
|
|
_cachedWorldChunks[activeChunkIndex] = chunk;
|
|
}
|
|
|
|
Debug.Assert(_activeChunkIndices.Count == NumChunkRows * NumChunkColumns);
|
|
|
|
foreach (Vector2 chunkKey in _activeChunkIndices)
|
|
if (!oldCachedChunks.ContainsKey(chunkKey))
|
|
{
|
|
ActivateChunk(_cachedWorldChunks[chunkKey], chunkKey);
|
|
State = GenerationState.Heightmap;
|
|
}
|
|
}
|
|
|
|
private void ActivateChunk(WorldChunk chunk, Vector2 chunkIndex)
|
|
{
|
|
chunk.SetChunkIndex(chunkIndex, HexGrid);
|
|
chunk.UpdateTileTransforms();
|
|
|
|
_addedChunkIndices.Add(chunk.ChunkIndex);
|
|
GD.Print("Generating noise for chunk " + chunk.ChunkIndex);
|
|
GenerateChunkNoiseMap(chunk);
|
|
}
|
|
|
|
private void DeactivateChunk(WorldChunk chunk)
|
|
{
|
|
GD.Print("Clearing chunk index: " + chunk.ChunkIndex);
|
|
_cachedWorldChunks.Remove(chunk.ChunkIndex);
|
|
chunk.ClearContent();
|
|
_unusedWorldChunks.Add(chunk);
|
|
}
|
|
|
|
private void GenerateChunkNoiseMap(WorldChunk chunk)
|
|
{
|
|
Vector2 chunkIndex = chunk.ChunkIndex;
|
|
|
|
ImageTexture noiseImageTexture = new();
|
|
noiseImageTexture.CreateFromImage(_noiseGenerator.GetImage(ChunkSize, ChunkSize, chunkIndex * ChunkSize),
|
|
0);
|
|
|
|
chunk.SetNoisemap(noiseImageTexture);
|
|
}
|
|
|
|
private void RemoveChunk(Vector2 cachedChunkKey)
|
|
{
|
|
_cachedWorldChunks.Remove(cachedChunkKey);
|
|
_removedChunkIndices.Add(cachedChunkKey);
|
|
|
|
foreach (WorldChunk chunk in Chunks.GetChildren())
|
|
if (chunk.ChunkIndex == new Vector2(cachedChunkKey.x, cachedChunkKey.y))
|
|
chunk.QueueFree();
|
|
}
|
|
|
|
|
|
private Vector2 GetChunkTupleFromPlaneCoord(Vector2 planeCoord)
|
|
{
|
|
HexCell centerOffsetCoord = HexGrid.GetHexAt(planeCoord);
|
|
return (centerOffsetCoord.OffsetCoords / ChunkSize).Floor();
|
|
}
|
|
|
|
public void SetCenterPlaneCoord(Vector2 centerPlaneCoord)
|
|
{
|
|
if (!_centerChunkRect.HasPoint(centerPlaneCoord))
|
|
{
|
|
UpdateCenterChunkFromPlaneCoord(centerPlaneCoord);
|
|
|
|
UpdateChunkBounds();
|
|
|
|
UpdateNavigationBounds();
|
|
}
|
|
}
|
|
|
|
private void UpdateWorldViewTexture()
|
|
{
|
|
int worldChunkSize = ChunkSize;
|
|
int numWorldChunkRows = NumChunkRows;
|
|
int numWorldChunkColumns = NumChunkColumns;
|
|
|
|
_heightmapImage.Create(worldChunkSize * numWorldChunkColumns, worldChunkSize * numWorldChunkRows, false,
|
|
Image.Format.Rgba8);
|
|
_tileTypeMapImage.Create(worldChunkSize * numWorldChunkColumns, worldChunkSize * numWorldChunkRows, false,
|
|
Image.Format.Rgba8);
|
|
|
|
foreach (Vector2 chunkIndex in _activeChunkIndices)
|
|
{
|
|
WorldChunk worldChunk = GetOrCreateWorldChunk(chunkIndex, Colors.White);
|
|
|
|
_heightmapImage.BlendRect(
|
|
worldChunk.HeightmapOffscreenViewport.GetTexture().GetData(),
|
|
new Rect2(Vector2.Zero, Vector2.One * worldChunkSize),
|
|
(chunkIndex - CenterChunkIndex + Vector2.One) * worldChunkSize);
|
|
|
|
_tileTypeMapImage.BlendRect(
|
|
worldChunk.TileTypeOffscreenViewport.GetTexture().GetData(),
|
|
new Rect2(Vector2.Zero, Vector2.One * worldChunkSize),
|
|
(chunkIndex - CenterChunkIndex + Vector2.One) * worldChunkSize);
|
|
}
|
|
|
|
_heightmapTexture = new ImageTexture();
|
|
_heightmapTexture.CreateFromImage(_heightmapImage);
|
|
|
|
_viewTileTypeTexture = new ImageTexture();
|
|
_viewTileTypeTexture.CreateFromImage(_tileTypeMapImage);
|
|
|
|
WorldTextureCoordinateOffset = _chunkIndexSouthWest * worldChunkSize;
|
|
|
|
EmitSignal("OnWorldViewTileTypeImageChanged", _tileTypeMapImage);
|
|
EmitSignal("OnHeightmapImageChanged", _heightmapImage);
|
|
}
|
|
|
|
private void UpdateChunkBounds()
|
|
{
|
|
_chunkIndexSouthWest = Vector2.Inf;
|
|
_chunkIndexNorthEast = -Vector2.Inf;
|
|
|
|
foreach (Vector2 chunkIndex in _activeChunkIndices)
|
|
{
|
|
WorldChunk worldChunk = GetOrCreateWorldChunk(chunkIndex, Colors.White);
|
|
|
|
if (chunkIndex.x <= _chunkIndexSouthWest.x && chunkIndex.y <= _chunkIndexSouthWest.y)
|
|
_chunkIndexSouthWest = chunkIndex;
|
|
else if (chunkIndex.x >= _chunkIndexNorthEast.x && chunkIndex.y >= _chunkIndexNorthEast.y)
|
|
_chunkIndexNorthEast = chunkIndex;
|
|
}
|
|
}
|
|
|
|
private void UpdateNavigationBounds()
|
|
{
|
|
HexCell cellSouthWest = HexGrid.GetHexAtOffset(_chunkIndexSouthWest * ChunkSize);
|
|
// Chunks have their cells ordered from south west (0,0) to north east (ChunkSize, ChunkSize). For the
|
|
// north east cell we have to add the chunk size to get to the actual corner cell.
|
|
HexCell cellNorthEast =
|
|
HexGrid.GetHexAtOffset(_chunkIndexNorthEast * ChunkSize + Vector2.One * (ChunkSize - 1));
|
|
|
|
HexCell centerCell =
|
|
HexGrid.GetHexAtOffset(((cellNorthEast.OffsetCoords - cellSouthWest.OffsetCoords) / 2).Round());
|
|
int numCells = ChunkSize * Math.Max(NumChunkColumns, NumChunkRows);
|
|
|
|
HexGrid.SetBoundsOffset(cellSouthWest, ChunkSize * new Vector2(NumChunkColumns, NumChunkRows));
|
|
}
|
|
|
|
public override void _Process(float delta)
|
|
{
|
|
GenerationState oldState = State;
|
|
|
|
UpdateGenerationState();
|
|
|
|
if (oldState != GenerationState.Done && State == GenerationState.Done)
|
|
UpdateWorldViewTexture();
|
|
}
|
|
|
|
private void UpdateGenerationState()
|
|
{
|
|
FrameCounter++;
|
|
|
|
if (State == GenerationState.Heightmap)
|
|
{
|
|
int numChunksGeneratingHeightmap = 0;
|
|
foreach (Vector2 chunkIndex in _addedChunkIndices)
|
|
{
|
|
WorldChunk chunk = _cachedWorldChunks[chunkIndex];
|
|
if (chunk.HeightMapFrameCount > 0) numChunksGeneratingHeightmap++;
|
|
}
|
|
|
|
if (numChunksGeneratingHeightmap == 0)
|
|
{
|
|
// assign height map images
|
|
foreach (Vector2 chunkIndex in _addedChunkIndices)
|
|
{
|
|
WorldChunk chunk = _cachedWorldChunks[chunkIndex];
|
|
chunk.SetHeightmap(chunk.HeightmapOffscreenViewport.GetTexture());
|
|
}
|
|
|
|
GD.Print("Switching to TileType Generation: " + FrameCounter);
|
|
State = GenerationState.TileType;
|
|
}
|
|
}
|
|
else if (State == GenerationState.TileType)
|
|
{
|
|
int numChunksGeneratingTileType = 0;
|
|
foreach (Vector2 chunkIndex in _addedChunkIndices)
|
|
{
|
|
WorldChunk chunk = _cachedWorldChunks[chunkIndex];
|
|
if (chunk.TileTypeMapFrameCount > 0) numChunksGeneratingTileType++;
|
|
}
|
|
|
|
if (numChunksGeneratingTileType == 0)
|
|
{
|
|
GD.Print("Switching to Object Generation: " + FrameCounter);
|
|
State = GenerationState.Objects;
|
|
}
|
|
}
|
|
else if (State == GenerationState.Objects)
|
|
{
|
|
// generate objects
|
|
foreach (Vector2 chunkIndex in _addedChunkIndices)
|
|
PopulateChunk(_cachedWorldChunks[chunkIndex]);
|
|
|
|
_addedChunkIndices.Clear();
|
|
|
|
GD.Print("Generation done: " + FrameCounter);
|
|
State = GenerationState.Done;
|
|
}
|
|
}
|
|
|
|
private void OnEntityClicked(Entity entity)
|
|
{
|
|
EmitSignal("EntityClicked", entity);
|
|
}
|
|
|
|
public void OnTileClicked(HexTile3D tile)
|
|
{
|
|
EmitSignal("TileClicked", tile);
|
|
}
|
|
|
|
public void OnTileHovered(HexTile3D tile)
|
|
{
|
|
EmitSignal("TileHovered", tile);
|
|
}
|
|
} |