Moved path planning to world class.

- Tree entities now are obstacles in navigation planning.
 - Once chopped obstacles are removed.
main
Martin Felis 2023-11-18 22:11:34 +01:00
parent a37b028b39
commit 2109c6b6ec
7 changed files with 452 additions and 488 deletions

View File

@ -7,8 +7,7 @@ using GodotComponentTest.utils;
/// <summary>
/// </summary>
public class NavigationComponent : Spatial
{
public class NavigationComponent : Spatial {
public World World { set; get; }
public Vector3 CurrentGoalPositionWorld { get; private set; } = Vector3.Zero;
public float CurrentGoalAngleWorld { get; private set; }
@ -21,39 +20,29 @@ public class NavigationComponent : Spatial
private List<NavigationPoint> _planningPathWorldNavigationPoints = new();
private List<NavigationPoint> _smoothedPathWorldNavigationPoints = new();
public override void _Ready()
{
public override void _Ready() {
base._Ready();
_pathWorldNavigationPoints = new List<NavigationPoint>();
}
public override void _Process(float delta)
{
public override void _Process(float delta) {
Debug.Assert(World != null);
}
public void PlanSmoothedPath(KinematicBody body, Transform fromTransformWorld, NavigationPoint navigationPoint)
{
public void PlanSmoothedPath(Entity body, Transform fromTransformWorld, NavigationPoint navigationPoint) {
if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)
&& navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation))
{
&& navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) {
FindPath(body, fromTransformWorld.origin, navigationPoint);
}
else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position))
{
} else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) {
FindPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition);
}
else
{
} else {
throw new NotImplementedException();
}
}
public void FindPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld)
{
var fromCell = World.HexGrid.GetHexAt(new Vector2(fromPositionWorld.x, fromPositionWorld.z));
if (World.HexGrid.GetHexCost(fromCell) == 0)
{
public void FindPath(Entity entity, Vector3 fromPositionWorld, Vector3 toPositionWorld) {
HexCell fromCell = World.HexGrid.GetHexAt(new Vector2(fromPositionWorld.x, fromPositionWorld.z));
if (World.HexGrid.GetHexCost(fromCell) == 0) {
GD.Print("Invalid starting point for FindPath(): returning empty path.");
_planningPathWorldNavigationPoints = new List<NavigationPoint>();
_planningPathWorldNavigationPoints.Add(new NavigationPoint(fromPositionWorld));
@ -61,11 +50,10 @@ public class NavigationComponent : Spatial
return;
}
var toCell = World.HexGrid.GetHexAt(new Vector2(toPositionWorld.x, toPositionWorld.z));
HexCell toCell = World.HexGrid.GetHexAt(new Vector2(toPositionWorld.x, toPositionWorld.z));
toCell = World.HexGrid.GetClosestWalkableCell(fromCell, toCell);
if (World.HexGrid.GetHexCost(toCell) == 0)
{
if (World.HexGrid.GetHexCost(toCell) == 0) {
GD.Print("Invalid target point for FindPath(): returning empty path.");
_planningPathWorldNavigationPoints = new List<NavigationPoint>();
_planningPathWorldNavigationPoints.Add(new NavigationPoint(fromPositionWorld));
@ -73,12 +61,11 @@ public class NavigationComponent : Spatial
return;
}
var path = World.HexGrid.FindPath(fromCell, toCell);
List<HexCell> path = World.FindPath(entity, fromCell, toCell);
// Generate grid navigation points
_planningPathWorldNavigationPoints = new List<NavigationPoint>();
foreach (var index in Enumerable.Range(0, path.Count))
{
foreach (int index in Enumerable.Range(0, path.Count)) {
_planningPathWorldNavigationPoints.Add(
new NavigationPoint(World.HexGrid.GetHexCenterVec3FromOffset(path[index].OffsetCoords)));
}
@ -86,40 +73,38 @@ public class NavigationComponent : Spatial
// Ensure the last point coincides with the target position
if (_planningPathWorldNavigationPoints.Count > 0 &&
(_planningPathWorldNavigationPoints.Last().WorldPosition - toPositionWorld).LengthSquared() <
0.5f * 0.5f)
{
0.5f * 0.5f) {
_planningPathWorldNavigationPoints[_planningPathWorldNavigationPoints.Count - 1].WorldPosition =
toPositionWorld;
}
// Perform smoothing
_planningPathSmoothedWorldNavigationPoints = SmoothPath(body, _planningPathWorldNavigationPoints);
_planningPathSmoothedWorldNavigationPoints = SmoothPath(entity, _planningPathWorldNavigationPoints);
// Ensure starting point is the current position
if (_planningPathSmoothedWorldNavigationPoints.Count > 0)
{
if (_planningPathSmoothedWorldNavigationPoints.Count > 0) {
_planningPathSmoothedWorldNavigationPoints[0] = new NavigationPoint(fromPositionWorld);
}
}
public void FindPath(KinematicBody body, Vector3 fromPositionWorld, NavigationPoint navigationPoint)
{
FindPath(body, fromPositionWorld, navigationPoint.WorldPosition);
public void FindPath(Entity entity, Vector3 fromPositionWorld, NavigationPoint navigationPoint) {
FindPath(entity, fromPositionWorld, navigationPoint.WorldPosition);
_planningPathWorldNavigationPoints[_planningPathWorldNavigationPoints.Count - 1] = navigationPoint;
_planningPathSmoothedWorldNavigationPoints[_planningPathSmoothedWorldNavigationPoints.Count - 1] =
navigationPoint;
if (_planningPathSmoothedWorldNavigationPoints.Count > 0) {
_planningPathWorldNavigationPoints[_planningPathWorldNavigationPoints.Count - 1] = navigationPoint;
_planningPathSmoothedWorldNavigationPoints[_planningPathSmoothedWorldNavigationPoints.Count - 1] =
navigationPoint;
}
}
public void PlanGridPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld)
{
var fromPositionOffset = World.WorldToOffsetCoords(fromPositionWorld);
var toPositionOffset = World.WorldToOffsetCoords(toPositionWorld);
public void PlanGridPath(Entity entity, Vector3 fromPositionWorld, Vector3 toPositionWorld) {
Vector2 fromPositionOffset = World.WorldToOffsetCoords(fromPositionWorld);
Vector2 toPositionOffset = World.WorldToOffsetCoords(toPositionWorld);
var fromCell = new HexCell();
HexCell fromCell = new();
fromCell.OffsetCoords = fromPositionOffset;
var toCell = new HexCell();
HexCell toCell = new();
toCell.OffsetCoords = toPositionOffset;
_path = fromCell.LineTo(toCell);
@ -129,26 +114,23 @@ public class NavigationComponent : Spatial
_pathWorldNavigationPoints.Add(
new NavigationPoint(World.HexGrid.GetHexCenterVec3FromOffset(fromPositionOffset)));
foreach (var index in Enumerable.Range(1, _path.Length - 1))
{
foreach (int index in Enumerable.Range(1, _path.Length - 1)) {
_pathWorldNavigationPoints.Add(
new NavigationPoint(World.GetHexCenterFromOffset(_path[index].OffsetCoords)));
}
if ((fromPositionWorld - World.GetHexCenterFromOffset(toCell.OffsetCoords)).LengthSquared() >
Globals.EpsPositionSquared)
{
// Remove the last one, because it is only the position rounded to HexGrid coordinates.
if (_pathWorldNavigationPoints.Count > 0)
{
{
if (_pathWorldNavigationPoints.Count > 0) {
_pathWorldNavigationPoints.RemoveAt(_pathWorldNavigationPoints.Count - 1);
}
}
_pathWorldNavigationPoints.Add(new NavigationPoint(toPositionWorld));
if (_pathWorldNavigationPoints.Count > 2)
{
_smoothedPathWorldNavigationPoints = SmoothPath(body, _pathWorldNavigationPoints);
if (_pathWorldNavigationPoints.Count > 2) {
_smoothedPathWorldNavigationPoints = SmoothPath(entity, _pathWorldNavigationPoints);
_pathWorldNavigationPoints = _smoothedPathWorldNavigationPoints;
}
@ -156,36 +138,28 @@ public class NavigationComponent : Spatial
}
public void PlanGridPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld,
Quat toWorldOrientation)
{
PlanGridPath(body, fromPositionWorld, toPositionWorld);
public void PlanGridPath(Entity entity, Vector3 fromPositionWorld, Vector3 toPositionWorld,
Quat toWorldOrientation) {
PlanGridPath(entity, fromPositionWorld, toPositionWorld);
_pathWorldNavigationPoints.Add(new NavigationPoint(toWorldOrientation));
}
public void PlanGridPath(KinematicBody body, Transform fromTransformWorld, NavigationPoint navigationPoint)
{
public void PlanGridPath(Entity entity, Transform fromTransformWorld, NavigationPoint navigationPoint) {
if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)
&& navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation))
{
PlanGridPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition,
&& navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) {
PlanGridPath(entity, fromTransformWorld.origin, navigationPoint.WorldPosition,
navigationPoint.WorldOrientation);
}
else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position))
{
PlanGridPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition);
}
else
{
} else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) {
PlanGridPath(entity, fromTransformWorld.origin, navigationPoint.WorldPosition);
} else {
throw new NotImplementedException();
}
}
public void PlanDirectPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld)
{
public void PlanDirectPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld) {
_pathWorldNavigationPoints.Clear();
_pathWorldNavigationPoints.Add(new NavigationPoint(toPositionWorld));
@ -194,23 +168,20 @@ public class NavigationComponent : Spatial
public void PlanDirectPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld,
Quat toWorldOrientation)
{
Quat toWorldOrientation) {
PlanDirectPath(body, fromPositionWorld, toPositionWorld);
_pathWorldNavigationPoints.Add(new NavigationPoint(toWorldOrientation));
}
public bool HasPathCollision(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld)
{
public bool HasPathCollision(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld) {
Vector3 fromPositionLocal = GlobalTransform.XformInv(fromPositionWorld);
Vector3 toPositionLocal = GlobalTransform.XformInv(toPositionWorld);
Vector3 relativeVelocity = GlobalTransform.basis.Xform(toPositionLocal - fromPositionLocal);
KinematicCollision moveCollision = body.MoveAndCollide(relativeVelocity, true, true, true);
if (moveCollision != null)
{
if (moveCollision != null) {
Spatial colliderSpatial = moveCollision.Collider as Spatial;
// GD.Print("Found collision: " + moveCollision.Collider + " (" + colliderSpatial.Name + ")");
return true;
@ -220,37 +191,30 @@ public class NavigationComponent : Spatial
}
public bool CheckSweptTriangleCellCollision(Vector3 startWorld, Vector3 endWorld, float radius)
{
Vector2 startPlane = new Vector2(startWorld.x, startWorld.z);
Vector2 endPlane = new Vector2(endWorld.x, endWorld.z);
public bool CheckSweptTriangleCellCollision(Vector3 startWorld, Vector3 endWorld, float radius) {
Vector2 startPlane = new(startWorld.x, startWorld.z);
Vector2 endPlane = new(endWorld.x, endWorld.z);
Vector2 directionPlane = (endPlane - startPlane).Normalized();
Vector2 sidePlane = directionPlane.Rotated(Mathf.Pi * 0.5f);
List<HexCell> cells =
World.HexGrid.GetCellsForLine(startPlane + directionPlane * radius, endPlane + directionPlane * radius);
foreach (HexCell cell in cells)
{
if (World.HexGrid.GetHexCost(cell) == 0)
{
foreach (HexCell cell in cells) {
if (World.HexGrid.GetHexCost(cell) == 0) {
return true;
}
}
cells = World.HexGrid.GetCellsForLine(startPlane + sidePlane * radius, endPlane + sidePlane * radius);
foreach (HexCell cell in cells)
{
if (World.HexGrid.GetHexCost(cell) == 0)
{
foreach (HexCell cell in cells) {
if (World.HexGrid.GetHexCost(cell) == 0) {
return true;
}
}
cells = World.HexGrid.GetCellsForLine(startPlane - sidePlane * radius, endPlane - sidePlane * radius);
foreach (HexCell cell in cells)
{
if (World.HexGrid.GetHexCost(cell) == 0)
{
foreach (HexCell cell in cells) {
if (World.HexGrid.GetHexCost(cell) == 0) {
return true;
}
}
@ -258,29 +222,24 @@ public class NavigationComponent : Spatial
return false;
}
public List<NavigationPoint> SmoothPath(KinematicBody body, List<NavigationPoint> navigationPoints)
{
if (navigationPoints.Count <= 2)
{
public List<NavigationPoint> SmoothPath(KinematicBody body, List<NavigationPoint> navigationPoints) {
if (navigationPoints.Count <= 2) {
return navigationPoints;
}
Vector3 bodyGlobalTranslation = body.GlobalTranslation;
List<NavigationPoint> smoothedPath = new List<NavigationPoint>();
List<NavigationPoint> smoothedPath = new();
int startIndex = 0;
int endIndex = navigationPoints.Count > 1 ? 1 : 0;
smoothedPath.Add(navigationPoints[startIndex]);
Vector3 startPoint = navigationPoints[startIndex].WorldPosition;
while (endIndex != navigationPoints.Count)
{
while (endIndex != navigationPoints.Count) {
Vector3 endPoint = navigationPoints[endIndex].WorldPosition;
if (CheckSweptTriangleCellCollision(startPoint, endPoint, 0.27f))
{
if (endIndex - startIndex == 1)
{
if (CheckSweptTriangleCellCollision(startPoint, endPoint, 0.27f)) {
if (endIndex - startIndex == 1) {
GD.Print("Aborting SmoothPath: input path passes through collision geometry.");
body.GlobalTranslation = bodyGlobalTranslation;
return smoothedPath;
@ -294,8 +253,7 @@ public class NavigationComponent : Spatial
continue;
}
if (endIndex == navigationPoints.Count - 1)
{
if (endIndex == navigationPoints.Count - 1) {
break;
}
@ -308,34 +266,25 @@ public class NavigationComponent : Spatial
return smoothedPath;
}
public void PlanDirectPath(KinematicBody body, Transform fromTransformWorld, NavigationPoint navigationPoint)
{
public void PlanDirectPath(KinematicBody body, Transform fromTransformWorld, NavigationPoint navigationPoint) {
if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)
&& navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation))
{
&& navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) {
PlanDirectPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition,
navigationPoint.WorldOrientation);
}
else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position))
{
} else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) {
PlanDirectPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition);
}
else
{
} else {
throw new NotImplementedException();
}
}
public void ActivatePlannedPath()
{
public void ActivatePlannedPath() {
_pathWorldNavigationPoints = _planningPathSmoothedWorldNavigationPoints;
UpdateCurrentGoal();
}
private void UpdateCurrentGoal()
{
if (_pathWorldNavigationPoints.Count == 0)
{
private void UpdateCurrentGoal() {
if (_pathWorldNavigationPoints.Count == 0) {
return;
}
@ -344,73 +293,56 @@ public class NavigationComponent : Spatial
CurrentGoalAngleWorld = _pathWorldNavigationPoints[0].WorldAngle;
}
private void ApplyExistingTransform(Transform worldTransform)
{
if (_currentGoal.Flags == NavigationPoint.NavigationFlags.Orientation)
{
private void ApplyExistingTransform(Transform worldTransform) {
if (_currentGoal.Flags == NavigationPoint.NavigationFlags.Orientation) {
CurrentGoalPositionWorld = worldTransform.origin;
}
else if (_currentGoal.Flags == NavigationPoint.NavigationFlags.Position)
{
} else if (_currentGoal.Flags == NavigationPoint.NavigationFlags.Position) {
CurrentGoalAngleWorld = Globals.CalcPlaneAngle(worldTransform);
}
}
public void UpdateCurrentGoal(Transform currentTransformWorld)
{
if (_currentGoal == null)
{
public void UpdateCurrentGoal(Transform currentTransformWorld) {
if (_currentGoal == null) {
_currentGoal = new NavigationPoint(currentTransformWorld);
}
if (_pathWorldNavigationPoints.Count == 0)
{
if (_pathWorldNavigationPoints.Count == 0) {
CurrentGoalAngleWorld = Globals.CalcPlaneAngle(currentTransformWorld);
CurrentGoalPositionWorld = currentTransformWorld.origin;
return;
}
if (_currentGoal.Flags.HasFlag(NavigationPoint.NavigationFlags.Position))
{
if (_currentGoal.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) {
CurrentGoalPositionWorld = _pathWorldNavigationPoints[0].WorldPosition;
}
else
{
} else {
CurrentGoalAngleWorld = Globals.CalcPlaneAngle(currentTransformWorld);
}
if (_currentGoal.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation))
{
if (_currentGoal.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) {
CurrentGoalAngleWorld = _currentGoal.WorldAngle;
}
else
{
} else {
CurrentGoalAngleWorld = Globals.CalcPlaneAngle(currentTransformWorld);
}
if (_currentGoal.IsReached(currentTransformWorld))
{
if (_currentGoal.IsReached(currentTransformWorld)) {
_pathWorldNavigationPoints.RemoveAt(0);
UpdateCurrentGoal();
ApplyExistingTransform(currentTransformWorld);
}
if (_pathWorldNavigationPoints.Count == 0)
{
if (_pathWorldNavigationPoints.Count == 0) {
CurrentGoalPositionWorld = currentTransformWorld.origin;
CurrentGoalAngleWorld = Globals.CalcPlaneAngle(currentTransformWorld);
}
}
public bool IsGoalReached()
{
public bool IsGoalReached() {
return _pathWorldNavigationPoints.Count == 0;
}
public void DebugDraw(Spatial parentNode, DebugGeometry debugGeometry)
{
public void DebugDraw(Spatial parentNode, DebugGeometry debugGeometry) {
Vector3 yOffset = Vector3.Up * 0.1f;
debugGeometry.GlobalTransform = Transform.Identity;
@ -428,8 +360,7 @@ public class NavigationComponent : Spatial
debugGeometry.PopTransform();
Vector3 previousPoint = parentNode.GlobalTranslation;
foreach (NavigationPoint point in _pathWorldNavigationPoints)
{
foreach (NavigationPoint point in _pathWorldNavigationPoints) {
debugGeometry.AddVertex(previousPoint + yOffset);
debugGeometry.AddVertex(point.WorldPosition + yOffset);
@ -437,8 +368,7 @@ public class NavigationComponent : Spatial
}
previousPoint = parentNode.GlobalTranslation;
foreach (NavigationPoint point in _smoothedPathWorldNavigationPoints)
{
foreach (NavigationPoint point in _smoothedPathWorldNavigationPoints) {
debugGeometry.SetColor(new Color(0, 0, 1));
debugGeometry.AddVertex(previousPoint + yOffset);
debugGeometry.AddVertex(point.WorldPosition + yOffset);
@ -447,8 +377,7 @@ public class NavigationComponent : Spatial
}
previousPoint = parentNode.GlobalTranslation;
foreach (NavigationPoint point in _planningPathWorldNavigationPoints)
{
foreach (NavigationPoint point in _planningPathWorldNavigationPoints) {
debugGeometry.SetColor(new Color(1, 0, 1));
debugGeometry.AddVertex(previousPoint + yOffset);
debugGeometry.AddVertex(point.WorldPosition + yOffset);
@ -457,8 +386,7 @@ public class NavigationComponent : Spatial
}
previousPoint = parentNode.GlobalTranslation;
foreach (NavigationPoint point in _planningPathSmoothedWorldNavigationPoints)
{
foreach (NavigationPoint point in _planningPathSmoothedWorldNavigationPoints) {
debugGeometry.SetColor(new Color(1, 1, 0));
debugGeometry.AddVertex(previousPoint + yOffset);
debugGeometry.AddVertex(point.WorldPosition + yOffset);

View File

@ -1,30 +1,24 @@
using Godot;
using System;
public class Entity : KinematicBody
{
public class Entity : KinematicBody {
public Vector3 Velocity { get; set; } = Vector3.Zero;
public float RotationalVelocity { get; set; } = 0;
/** Defines the angle in plane coordinates, 0 => pointing to the right/east, pi/2 pointing up/north, range [-pi,pi]. */
public float PlaneAngle
{
/**
* Defines the angle in plane coordinates, 0 => pointing to the right/east, pi/2 pointing up/north, range [-pi,pi].
*/
public float PlaneAngle {
get => Globals.CalcPlaneAngle(GlobalTransform);
set => GlobalTransform = new Transform(new Basis(Vector3.Up, value + Mathf.Pi * 0.5f), GlobalTranslation);
}
public float CalcShortestPlaneRotationToTargetDirection(Vector3 globalTargetDirection)
{
public float CalcShortestPlaneRotationToTargetDirection(Vector3 globalTargetDirection) {
float angleToTarget = Vector3.Right.SignedAngleTo(globalTargetDirection, Vector3.Up);
float currentAngle = PlaneAngle;
float delta = angleToTarget - currentAngle;
delta += (delta > Mathf.Pi) ? -Mathf.Pi * 2 : (delta < -Mathf.Pi) ? Mathf.Pi * 2 : 0;
delta += delta > Mathf.Pi ? -Mathf.Pi * 2 : delta < -Mathf.Pi ? Mathf.Pi * 2 : 0;
return delta;
}
public override void _Ready()
{
}
}

View File

@ -4,8 +4,7 @@ using Godot;
using GodotComponentTest.components;
using GodotComponentTest.entities;
public class Tree : StaticBody, IInteractionInterface
{
public class Tree : StaticBody, IInteractionInterface {
[Export] public float ChopDuration = 2;
public bool IsMouseOver;
public InteractionComponent InteractionComponent { get; set; }
@ -18,9 +17,11 @@ public class Tree : StaticBody, IInteractionInterface
[Signal]
public delegate void EntityClicked(Entity entity);
[Signal]
public delegate void TreeChopped(Tree entity);
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
public override void _Ready() {
_geometry = GetNode<MeshInstance>("Geometry/tree");
Debug.Assert(_geometry != null);
_animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
@ -33,56 +34,43 @@ public class Tree : StaticBody, IInteractionInterface
Connect("mouse_exited", this, nameof(OnAreaMouseExited));
}
public override void _Process(float delta)
{
public override void _Process(float delta) {
base._Process(delta);
if (_isBeingChopped)
{
if (_isBeingChopped) {
_health = Math.Max(0, _health - delta * 100 / ChopDuration);
}
if (_health == 0)
{
if (_health == 0) {
InteractionComponent.EndInteraction();
InteractionComponent.TargetEntity = null;
QueueFree();
EmitSignal("TreeChopped", this);
}
}
public void OnAreaInputEvent(Node camera, InputEvent inputEvent, Vector3 position, Vector3 normal,
int shapeIndex)
{
if (IsMouseOver && inputEvent is InputEventMouseButton)
{
int shapeIndex) {
if (IsMouseOver && inputEvent is InputEventMouseButton) {
InputEventMouseButton mouseButtonEvent = (InputEventMouseButton)inputEvent;
if (mouseButtonEvent.ButtonIndex == 1 && mouseButtonEvent.Pressed)
{
if (mouseButtonEvent.ButtonIndex == 1 && mouseButtonEvent.Pressed) {
EmitSignal("EntityClicked", this);
}
}
}
public void OnAreaMouseEntered()
{
public void OnAreaMouseEntered() {
IsMouseOver = true;
SpatialMaterial overrideMaterial = new SpatialMaterial();
SpatialMaterial overrideMaterial = new();
overrideMaterial.AlbedoColor = new Color(1, 0, 0);
_geometry.MaterialOverride = overrideMaterial;
}
public void OnAreaMouseExited()
{
public void OnAreaMouseExited() {
IsMouseOver = false;
_geometry.MaterialOverride = null;
}
public void OnInteractionStart()
{
public void OnInteractionStart() {
GD.Print("Starting tree animationplayer");
_animationPlayer.CurrentAnimation = "TreeShake";
_animationPlayer.Seek(0);
@ -90,11 +78,10 @@ public class Tree : StaticBody, IInteractionInterface
_isBeingChopped = true;
}
public void OnInteractionEnd()
{
public void OnInteractionEnd() {
_animationPlayer.CurrentAnimation = "Idle";
_animationPlayer.Seek(0);
_animationPlayer.Stop();
_isBeingChopped = false;
}
}
}

View File

@ -30,7 +30,7 @@ keystore/release="/home/martin/projects/GodotComponentTest/android/keystore/debu
keystore/release_user="androiddebugkey"
keystore/release_password="android"
one_click_deploy/clear_previous_install=true
version/code=1
version/code=5
version/name="1.0"
package/unique_name="org.godotengine.$genname"
package/name=""

View File

@ -4,11 +4,10 @@ using System.Diagnostics;
using System.Linq;
using Godot;
using Godot.Collections;
using Priority_Queue;
public class World : Spatial
{
public enum GenerationState
{
public class World : Spatial {
public enum GenerationState {
Undefined,
Heightmap,
TileType,
@ -17,7 +16,7 @@ public class World : Spatial
}
// constants
public const int ChunkSize = 12;
public const int ChunkSize = 14;
public const int NumChunkRows = 3;
public const int NumChunkColumns = NumChunkRows;
private static readonly Color RockColor = new(0.5f, 0.5f, 0.4f);
@ -27,17 +26,17 @@ public class World : Spatial
private readonly Godot.Collections.Dictionary<Vector2, WorldChunk> _cachedWorldChunks;
private readonly List<Vector2> _addedChunkIndices = new();
private readonly List<WorldChunk> _unusedWorldChunks = new();
private readonly List<WorldChunk> _deactivatedWorldChunks = 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;
private readonly List<Spatial> _removedSpatialNodes = new();
// delegate void OnCoordClicked(Vector2 world_pos);
@ -93,16 +92,14 @@ public class World : Spatial
[Signal]
private delegate void TileHovered(HexTile3D tile3d);
public World()
{
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()
{
public override void _Ready() {
Chunks = (Spatial)FindNode("Chunks");
Debug.Assert(Chunks != null);
@ -117,19 +114,24 @@ public class World : Spatial
GetNode<Spatial>("Assets").Visible = false;
_rockAssets = new Array<Spatial>();
foreach (Spatial asset in GetNode<Node>("Assets/Rocks").GetChildren()) _rockAssets.Add(asset);
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);
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);
foreach (Spatial asset in GetNode<Node>("Assets/Trees").GetChildren()) {
_treeAssets.Add(asset);
}
SetCenterPlaneCoord(Vector2.Zero);
}
public void InitNoiseGenerator()
{
public void InitNoiseGenerator() {
_noiseGenerator = new OpenSimplexNoise();
_noiseGenerator.Seed = Seed;
@ -139,22 +141,17 @@ public class World : Spatial
_noiseGenerator.Lacunarity = 2;
}
public WorldChunk GetOrCreateWorldChunk(Vector2 chunkIndex, Color debugColor)
{
public WorldChunk GetOrCreateWorldChunk(Vector2 chunkIndex, Color debugColor) {
WorldChunk chunk;
if (IsChunkCached(chunkIndex))
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
{
if (_deactivatedWorldChunks.Count > 0) {
chunk = _deactivatedWorldChunks.First();
_deactivatedWorldChunks.RemoveAt(0);
} else {
chunk = CreateWorldChunk(chunkIndex, debugColor);
}
@ -163,14 +160,12 @@ public class World : Spatial
return chunk;
}
private bool IsChunkCached(Vector2 chunkIndex)
{
private bool IsChunkCached(Vector2 chunkIndex) {
return _cachedWorldChunks.ContainsKey(chunkIndex);
}
private WorldChunk CreateWorldChunk(Vector2 chunkIndex, Color debugColor)
{
private WorldChunk CreateWorldChunk(Vector2 chunkIndex, Color debugColor) {
WorldChunk result = (WorldChunk)_worldChunkScene.Instance();
Chunks.AddChild(result);
result.Connect("TileClicked", this, nameof(OnTileClicked));
@ -191,15 +186,16 @@ public class World : Spatial
return result;
}
private bool IsColorEqualApprox(Color colorA, Color colorB)
{
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;
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();
@ -214,41 +210,40 @@ public class World : Spatial
return assetInstance;
}
private void PopulateChunk(WorldChunk chunk)
{
private void PopulateChunk(WorldChunk chunk) {
Random environmentRandom = new(Seed);
Image tileTypeImage = chunk.TileTypeOffscreenViewport.GetTexture().GetData();
tileTypeImage.Lock();
chunk.CreateUnlockedTileTypeImage();
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;
foreach (int textureCoordU in Enumerable.Range(0, chunk.Size)) {
foreach (int textureCoordV in Enumerable.Range(0, chunk.Size)) {
Color colorValue = chunk.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);
if (IsColorEqualApprox(colorValue, RockColor)) {
Spatial rockAsset = SelectAsset(textureCoord, _rockAssets, environmentRandom, 0.15);
if (rockAsset != null) {
chunk.Entities.AddChild(rockAsset);
MarkCellUnwalkable(HexGrid.GetHexAtOffset(offsetCoord));
}
} 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));
}
Tree treeAsset = SelectAsset(textureCoord, _treeAssets, environmentRandom, 0.05) as Tree;
if (treeAsset != null) {
chunk.Entities.AddChild(treeAsset);
treeAsset.Connect("EntityClicked", this, nameof(OnEntityClicked));
treeAsset.Connect("TreeChopped", this, nameof(OnBlockingSpatialRemoved));
MarkCellUnwalkable(HexGrid.GetHexAtOffset(offsetCoord));
}
// TODO: MarkCellUnwalkable(cell);
// else if (environmentRandom.NextDouble() < 0.01)
// TODO: MarkCellUnwalkable(cell);
// else if (environmentRandom.NextDouble() < 0.01)
// {
// var chestAsset = (Chest)_chestScene.Instance();
// var assetTransform = Transform.Identity;
@ -258,36 +253,29 @@ public class World : Spatial
// Entities.AddChild(chestAsset);
// MarkCellUnwalkable(cell);
// }
}
}
// else if (IsColorWater(colorValue))
// {
// MarkCellUnwalkable(cell);
// }
}
}
tileTypeImage.Unlock();
}
public Vector2 WorldToOffsetCoords(Vector3 fromPositionWorld)
{
public Vector2 WorldToOffsetCoords(Vector3 fromPositionWorld) {
return HexGrid.GetHexAt(new Vector2(fromPositionWorld.x, fromPositionWorld.z)).OffsetCoords;
}
public Vector3 GetHexCenterFromOffset(Vector2 fromPositionOffset)
{
public Vector3 GetHexCenterFromOffset(Vector2 fromPositionOffset) {
return HexGrid.GetHexCenterVec3FromOffset(fromPositionOffset);
}
public void UpdateCenterChunkFromPlaneCoord(Vector2 planeCoord)
{
if (State != GenerationState.Done)
{
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);
@ -319,15 +307,16 @@ public class World : Spatial
_activeChunkIndices.Add(CenterChunkIndex + new Vector2(+1, +1));
// clear unused chunks
_unusedWorldChunks.Clear();
_deactivatedWorldChunks.Clear();
_addedChunkIndices.Clear();
foreach (Vector2 oldChunkIndex in oldCachedChunks.Keys)
if (!_activeChunkIndices.Contains(oldChunkIndex))
foreach (Vector2 oldChunkIndex in oldCachedChunks.Keys) {
if (!_activeChunkIndices.Contains(oldChunkIndex)) {
DeactivateChunk(oldCachedChunks[oldChunkIndex]);
}
}
foreach (Vector2 activeChunkIndex in _activeChunkIndices)
{
foreach (Vector2 activeChunkIndex in _activeChunkIndices) {
WorldChunk chunk = GetOrCreateWorldChunk(activeChunkIndex,
new Color(GD.Randf(), GD.Randf(), GD.Randf()));
_cachedWorldChunks[activeChunkIndex] = chunk;
@ -335,34 +324,29 @@ public class World : Spatial
Debug.Assert(_activeChunkIndices.Count == NumChunkRows * NumChunkColumns);
foreach (Vector2 chunkKey in _activeChunkIndices)
if (!oldCachedChunks.ContainsKey(chunkKey))
{
foreach (Vector2 chunkKey in _activeChunkIndices) {
if (!oldCachedChunks.ContainsKey(chunkKey)) {
ActivateChunk(_cachedWorldChunks[chunkKey], chunkKey);
State = GenerationState.Heightmap;
}
}
}
private void ActivateChunk(WorldChunk chunk, Vector2 chunkIndex)
{
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);
private void DeactivateChunk(WorldChunk chunk) {
_cachedWorldChunks.Remove(chunk.ChunkIndex);
chunk.ClearContent();
_unusedWorldChunks.Add(chunk);
_deactivatedWorldChunks.Add(chunk);
}
private void GenerateChunkNoiseMap(WorldChunk chunk)
{
private void GenerateChunkNoiseMap(WorldChunk chunk) {
Vector2 chunkIndex = chunk.ChunkIndex;
ImageTexture noiseImageTexture = new();
@ -372,27 +356,29 @@ public class World : Spatial
chunk.SetNoisemap(noiseImageTexture);
}
private void RemoveChunk(Vector2 cachedChunkKey)
{
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))
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();
private Vector2 GetChunkTupleFromPlaneCoord(Vector2 planeCoord) {
HexCell hexCell = HexGrid.GetHexAt(planeCoord);
return GetChunkIndexFromOffsetCoord(hexCell.OffsetCoords);
}
public void SetCenterPlaneCoord(Vector2 centerPlaneCoord)
{
if (!_centerChunkRect.HasPoint(centerPlaneCoord))
{
private Vector2 GetChunkIndexFromOffsetCoord(Vector2 offsetCoord) {
return (offsetCoord / ChunkSize).Floor();
}
public void SetCenterPlaneCoord(Vector2 centerPlaneCoord) {
if (!_centerChunkRect.HasPoint(centerPlaneCoord)) {
UpdateCenterChunkFromPlaneCoord(centerPlaneCoord);
UpdateChunkBounds();
@ -401,8 +387,7 @@ public class World : Spatial
}
}
private void UpdateWorldViewTexture()
{
private void UpdateWorldViewTexture() {
int worldChunkSize = ChunkSize;
int numWorldChunkRows = NumChunkRows;
int numWorldChunkColumns = NumChunkColumns;
@ -412,8 +397,7 @@ public class World : Spatial
_tileTypeMapImage.Create(worldChunkSize * numWorldChunkColumns, worldChunkSize * numWorldChunkRows, false,
Image.Format.Rgba8);
foreach (Vector2 chunkIndex in _activeChunkIndices)
{
foreach (Vector2 chunkIndex in _activeChunkIndices) {
WorldChunk worldChunk = GetOrCreateWorldChunk(chunkIndex, Colors.White);
_heightmapImage.BlendRect(
@ -439,113 +423,207 @@ public class World : Spatial
EmitSignal("OnHeightmapImageChanged", _heightmapImage);
}
private void UpdateChunkBounds()
{
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)
foreach (Vector2 chunkIndex in _activeChunkIndices) {
if (chunkIndex.x <= _chunkIndexSouthWest.x && chunkIndex.y <= _chunkIndexSouthWest.y) {
_chunkIndexSouthWest = chunkIndex;
else if (chunkIndex.x >= _chunkIndexNorthEast.x && chunkIndex.y >= _chunkIndexNorthEast.y)
} else if (chunkIndex.x >= _chunkIndexNorthEast.x && chunkIndex.y >= _chunkIndexNorthEast.y) {
_chunkIndexNorthEast = chunkIndex;
}
}
}
private void UpdateNavigationBounds()
{
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)
{
public void MarkCellUnwalkable(HexCell cell) {
HexGrid.AddObstacle(cell);
}
public float GetHexCost(Entity entity, HexCell cell) {
float nextHexCost = HexGrid.GetHexCost(cell);
if (nextHexCost != 0) {
Vector2 nextOffset = cell.OffsetCoords;
Vector2 chunkIndex = GetChunkIndexFromOffsetCoord(nextOffset);
WorldChunk chunk = _cachedWorldChunks[chunkIndex];
Vector2 textureCoordinate = nextOffset - Vector2.One * ChunkSize * chunkIndex;
Color tileTypeColor = chunk.TileTypeImage.GetPixel((int)textureCoordinate.x, (int)textureCoordinate.y);
if (!IsColorEqualApprox(tileTypeColor, GrassColor) &&
!IsColorEqualApprox(tileTypeColor, DarkGrassColor)) {
nextHexCost = 0;
}
}
return nextHexCost;
}
public float GetMoveCost(Entity entity, HexCell currentHex, HexCell nextHex) {
if (GetHexCost(entity, nextHex) == 0) {
return 0;
}
return HexGrid.GetMoveCost(currentHex.AxialCoords,
new HexCell(nextHex.AxialCoords - currentHex.AxialCoords).CubeCoords);
}
public List<HexCell> FindPath(Entity entity, HexCell startHex, HexCell goalHex) {
if (State != GenerationState.Done) {
return new List<HexCell>();
}
Vector2 goalAxialCoords = goalHex.AxialCoords;
SimplePriorityQueue<Vector2, float> frontier = new();
frontier.Enqueue(startHex.AxialCoords, 0);
System.Collections.Generic.Dictionary<Vector2, Vector2> cameFrom = new();
System.Collections.Generic.Dictionary<Vector2, float> costSoFar = new();
cameFrom.Add(startHex.AxialCoords, startHex.AxialCoords);
costSoFar.Add(startHex.AxialCoords, 0);
int FindPathCheckedCellCount = 0;
while (frontier.Any()) {
FindPathCheckedCellCount++;
HexCell currentHex = new(frontier.Dequeue());
Vector2 currentAxial = currentHex.AxialCoords;
if (currentHex == goalHex) {
break;
}
foreach (HexCell nextHex in currentHex.GetAllAdjacent()) {
Vector2 nextAxial = nextHex.AxialCoords;
float moveCost = GetMoveCost(entity, currentHex, nextHex);
if (nextHex == goalHex && moveCost == 0 && GetHexCost(entity, nextHex) == 0) {
// Goal ist an obstacle
cameFrom[nextHex.AxialCoords] = currentHex.AxialCoords;
frontier.Clear();
break;
}
if (moveCost == 0) {
continue;
}
moveCost += costSoFar[currentHex.AxialCoords];
if (!costSoFar.ContainsKey(nextHex.AxialCoords) || moveCost < costSoFar[nextHex.AxialCoords]) {
costSoFar[nextHex.AxialCoords] = moveCost;
float priority = moveCost + nextHex.DistanceTo(goalHex);
frontier.Enqueue(nextHex.AxialCoords, priority);
cameFrom[nextHex.AxialCoords] = currentHex.AxialCoords;
}
}
}
// GD.Print("Checked Cell Count: " + FindPathCheckedCellCount);
List<HexCell> result = new();
if (!cameFrom.ContainsKey(goalHex.AxialCoords)) {
GD.Print("Failed to find path from " + startHex + " to " + goalHex);
return result;
}
if (HexGrid.GetHexCost(goalAxialCoords) != 0) {
result.Add(goalHex);
}
HexCell pathHex = goalHex;
while (pathHex != startHex) {
pathHex = new HexCell(cameFrom[pathHex.AxialCoords]);
result.Insert(0, pathHex);
}
return result;
}
public override void _Process(float delta) {
GenerationState oldState = State;
UpdateGenerationState();
if (oldState != GenerationState.Done && State == GenerationState.Done)
if (oldState != GenerationState.Done && State == GenerationState.Done) {
UpdateWorldViewTexture();
}
while (_removedSpatialNodes.Count > 0) {
GD.Print("Queueing deletion of " + _removedSpatialNodes[0]);
_removedSpatialNodes[0].QueueFree();
_removedSpatialNodes.RemoveAt(0);
}
}
private void UpdateGenerationState()
{
FrameCounter++;
if (State == GenerationState.Heightmap)
{
private void UpdateGenerationState() {
if (State == GenerationState.Heightmap) {
int numChunksGeneratingHeightmap = 0;
foreach (Vector2 chunkIndex in _addedChunkIndices)
{
foreach (Vector2 chunkIndex in _addedChunkIndices) {
WorldChunk chunk = _cachedWorldChunks[chunkIndex];
if (chunk.HeightMapFrameCount > 0) numChunksGeneratingHeightmap++;
if (chunk.HeightMapFrameCount > 0) {
numChunksGeneratingHeightmap++;
}
}
if (numChunksGeneratingHeightmap == 0)
{
if (numChunksGeneratingHeightmap == 0) {
// assign height map images
foreach (Vector2 chunkIndex in _addedChunkIndices)
{
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)
{
} else if (State == GenerationState.TileType) {
int numChunksGeneratingTileType = 0;
foreach (Vector2 chunkIndex in _addedChunkIndices)
{
foreach (Vector2 chunkIndex in _addedChunkIndices) {
WorldChunk chunk = _cachedWorldChunks[chunkIndex];
if (chunk.TileTypeMapFrameCount > 0) numChunksGeneratingTileType++;
if (chunk.TileTypeMapFrameCount > 0) {
numChunksGeneratingTileType++;
}
}
if (numChunksGeneratingTileType == 0)
{
GD.Print("Switching to Object Generation: " + FrameCounter);
if (numChunksGeneratingTileType == 0) {
State = GenerationState.Objects;
}
}
else if (State == GenerationState.Objects)
{
} else if (State == GenerationState.Objects) {
// generate objects
foreach (Vector2 chunkIndex in _addedChunkIndices)
foreach (Vector2 chunkIndex in _addedChunkIndices) {
PopulateChunk(_cachedWorldChunks[chunkIndex]);
}
_addedChunkIndices.Clear();
GD.Print("Generation done: " + FrameCounter);
State = GenerationState.Done;
}
}
private void OnEntityClicked(Entity entity)
{
private void OnEntityClicked(Entity entity) {
EmitSignal("EntityClicked", entity);
}
public void OnTileClicked(HexTile3D tile)
{
public void OnTileClicked(HexTile3D tile) {
EmitSignal("TileClicked", tile);
}
public void OnTileHovered(HexTile3D tile)
{
public void OnTileHovered(HexTile3D tile) {
EmitSignal("TileHovered", tile);
}
public void OnBlockingSpatialRemoved(Spatial spatialNode) {
if (spatialNode.IsQueuedForDeletion()) {
return;
}
HexGrid.RemoveObstacle(HexGrid.GetHexAt(new Vector2(spatialNode.GlobalTranslation.x,
spatialNode.GlobalTranslation.z)));
_removedSpatialNodes.Add(spatialNode);
}
}

View File

@ -3,8 +3,7 @@ using System.Linq;
using Godot;
using Godot.Collections;
public class WorldChunk : Spatial
{
public class WorldChunk : Spatial {
private readonly PackedScene _hexTile3DScene = GD.Load<PackedScene>("res://scenes/HexTile3D.tscn");
private MultiMeshInstance _multiMeshInstance;
private readonly Array<int> _tileInstanceIndices = new();
@ -28,6 +27,7 @@ public class WorldChunk : Spatial
[Export] public Texture HeightMap;
public int HeightMapFrameCount;
public Image TileTypeImage;
public Viewport HeightmapOffscreenViewport;
[Export] public Texture NavigationMap;
@ -56,32 +56,30 @@ public class WorldChunk : Spatial
public int TileTypeMapFrameCount;
public Viewport TileTypeOffscreenViewport;
public WorldChunk()
{
}
public WorldChunk() { }
public WorldChunk(int size)
{
public WorldChunk(int size) {
SetSize(size);
}
[Export]
public bool ShowTextureOverlay
{
public bool ShowTextureOverlay {
get => _showTextureOverlay;
set
{
if (PlaneRectMesh != null) PlaneRectMesh.Visible = value;
set {
if (PlaneRectMesh != null) {
PlaneRectMesh.Visible = value;
}
}
}
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
public override void _Ready() {
PlaneRectMesh = (MeshInstance)FindNode("PlaneRectMesh");
Debug.Assert(PlaneRectMesh != null);
if (PlaneRectMesh.Visible) _showTextureOverlay = true;
if (PlaneRectMesh.Visible) {
_showTextureOverlay = true;
}
Transform planeRectTransform = Transform.Identity;
planeRectTransform =
@ -116,12 +114,10 @@ public class WorldChunk : Spatial
SetSize(World.ChunkSize);
}
public void SetSize(int size)
{
public void SetSize(int size) {
Size = size;
if (TileTypeOffscreenViewport != null)
{
if (TileTypeOffscreenViewport != null) {
TileTypeOffscreenViewport.Size = Vector2.One * size;
HeightmapOffscreenViewport.Size = Vector2.One * size;
_noiseMask.Transform = Transform2D.Identity.Scaled(Vector2.One * size / _noiseMask.Texture.GetSize().x);
@ -131,8 +127,7 @@ public class WorldChunk : Spatial
}
}
public void SetChunkIndex(Vector2 chunkIndex, HexGrid hexGrid)
{
public void SetChunkIndex(Vector2 chunkIndex, HexGrid hexGrid) {
ChunkIndex = chunkIndex;
float chunkSize = World.ChunkSize;
@ -148,49 +143,48 @@ public class WorldChunk : Spatial
new Vector2(localPlaneCoordSouthWest.x, localPlaneCoordNorthEast.y),
new Vector2(localPlaneCoordNorthEast.x - localPlaneCoordSouthWest.x,
localPlaneCoordSouthWest.y - localPlaneCoordNorthEast.y)
);
);
}
public void InitializeTileInstances(Vector2 chunkIndex, MultiMeshInstance multiMeshInstance,
int tileInstanceIndexStart)
{
int tileInstanceIndexStart) {
_multiMeshInstance = multiMeshInstance;
_tileInstanceIndices.Clear();
int chunkSize = World.ChunkSize;
foreach (Spatial node in Tiles.GetChildren())
foreach (Spatial node in Tiles.GetChildren()) {
node.QueueFree();
}
foreach (int i in Enumerable.Range(0, chunkSize))
foreach (int j in Enumerable.Range(0, chunkSize))
{
HexTile3D tile3D = (HexTile3D)_hexTile3DScene.Instance();
tile3D.Connect("TileClicked", this, nameof(OnTileClicked));
tile3D.Connect("TileHovered", this, nameof(OnTileHovered));
foreach (int i in Enumerable.Range(0, chunkSize)) {
foreach (int j in Enumerable.Range(0, chunkSize)) {
HexTile3D tile3D = (HexTile3D)_hexTile3DScene.Instance();
tile3D.Connect("TileClicked", this, nameof(OnTileClicked));
tile3D.Connect("TileHovered", this, nameof(OnTileHovered));
tile3D.Cell.OffsetCoords = new Vector2(chunkIndex * World.ChunkSize + new Vector2(i, j));
_tileInstanceIndices.Add(tileInstanceIndexStart + _tileInstanceIndices.Count);
tile3D.Cell.OffsetCoords = new Vector2(chunkIndex * World.ChunkSize + new Vector2(i, j));
_tileInstanceIndices.Add(tileInstanceIndexStart + _tileInstanceIndices.Count);
Transform tileTransform = Transform.Identity;
Vector2 centerPlaneCoord = _hexGrid.GetHexCenterFromOffset(new Vector2(i, j));
tileTransform.origin = new Vector3(centerPlaneCoord.x, 0, centerPlaneCoord.y);
tile3D.Transform = tileTransform;
Transform tileTransform = Transform.Identity;
Vector2 centerPlaneCoord = _hexGrid.GetHexCenterFromOffset(new Vector2(i, j));
tileTransform.origin = new Vector3(centerPlaneCoord.x, 0, centerPlaneCoord.y);
tile3D.Transform = tileTransform;
Tiles.AddChild(tile3D);
Tiles.AddChild(tile3D);
}
}
_multiMeshInstance.Multimesh.VisibleInstanceCount = _multiMeshInstance.Multimesh.InstanceCount;
}
public void ClearContent()
{
foreach (Spatial child in Entities.GetChildren())
public void ClearContent() {
foreach (Spatial child in Entities.GetChildren()) {
child.QueueFree();
}
}
public void UpdateTileTransforms()
{
public void UpdateTileTransforms() {
Transform chunkTransform = Transform.Identity;
Vector2 chunkOriginPlaneCoord = _hexGrid.GetHexCenterFromOffset(ChunkIndex * World.ChunkSize);
chunkTransform.origin = new Vector3(chunkOriginPlaneCoord.x, 0, chunkOriginPlaneCoord.y);
@ -198,10 +192,7 @@ public class WorldChunk : Spatial
Basis tileOrientation = new(Vector3.Up, 90f * Mathf.Pi / 180f);
GD.Print("Updating transforms for instances of chunk " + ChunkIndex + " origin: " + chunkTransform.origin);
foreach (int i in Enumerable.Range(0, _tileInstanceIndices.Count))
{
foreach (int i in Enumerable.Range(0, _tileInstanceIndices.Count)) {
int column = i % World.ChunkSize;
int row = i / World.ChunkSize;
@ -211,17 +202,17 @@ public class WorldChunk : Spatial
Transform hexTransform = new(tileOrientation,
chunkTransform.origin + new Vector3(tilePlaneCoord.x, 0, tilePlaneCoord.y));
if (_showHexTiles)
if (_showHexTiles) {
hexTransform = new Transform(tileOrientation.Scaled(Vector3.One * 0.95f),
hexTransform.origin);
}
_multiMeshInstance.Multimesh.SetInstanceTransform(_tileInstanceIndices[i], hexTransform);
}
}
// other members
public void SaveToFile(string chunkName)
{
public void SaveToFile(string chunkName) {
Image image = new();
image.CreateFromData(Size, Size, false, Image.Format.Rgba8, TileTypeMap.GetData().GetData());
@ -234,13 +225,10 @@ public class WorldChunk : Spatial
image.SavePng(chunkName + "_heightMap.png");
}
public void LoadFromFile(string chunkName)
{
}
public void LoadFromFile(string chunkName) { }
public void SetNoisemap(Texture texture)
{
public void SetNoisemap(Texture texture) {
_noiseSprite.Texture = texture;
_noiseSprite.Transform =
Transform2D.Identity.Scaled(HeightmapOffscreenViewport.Size / _noiseSprite.Texture.GetSize().x);
@ -248,8 +236,7 @@ public class WorldChunk : Spatial
HeightMapFrameCount = 1;
}
public void SetHeightmap(Texture texture)
{
public void SetHeightmap(Texture texture) {
_heightmapSprite.Texture = texture;
_heightmapSprite.Transform =
Transform2D.Identity.Scaled(TileTypeOffscreenViewport.Size / _heightmapSprite.Texture.GetSize());
@ -258,25 +245,29 @@ public class WorldChunk : Spatial
TileTypeMapFrameCount = 1;
}
public override void _Process(float delta)
{
public void CreateUnlockedTileTypeImage() {
TileTypeImage = TileTypeOffscreenViewport.GetTexture().GetData();
TileTypeImage.Lock();
}
public override void _Process(float delta) {
Texture tileTypeTexture = TileTypeOffscreenViewport.GetTexture();
if (NoiseTextureCheckerboardOverlay)
{
if (NoiseTextureCheckerboardOverlay) {
Image tileTypeImage = tileTypeTexture.GetData();
tileTypeImage.Lock();
foreach (int i in Enumerable.Range(0, Size))
foreach (int j in Enumerable.Range(0, Size))
{
Vector2 textureCoord = new(i, j);
Color baseColor = tileTypeImage.GetPixelv(textureCoord);
foreach (int i in Enumerable.Range(0, Size)) {
foreach (int j in Enumerable.Range(0, Size)) {
Vector2 textureCoord = new(i, j);
Color baseColor = tileTypeImage.GetPixelv(textureCoord);
if ((i + j) % 2 == 0)
tileTypeImage.SetPixelv(textureCoord, baseColor);
else
tileTypeImage.SetPixelv(textureCoord, baseColor * 0.6f);
if ((i + j) % 2 == 0) {
tileTypeImage.SetPixelv(textureCoord, baseColor);
} else {
tileTypeImage.SetPixelv(textureCoord, baseColor * 0.6f);
}
}
}
tileTypeImage.Unlock();
@ -295,24 +286,26 @@ public class WorldChunk : Spatial
//RectMaterial.Uv1Triplanar = true;
PlaneRectMesh.SetSurfaceMaterial(0, _rectMaterial);
if (HeightMapFrameCount == 0) HeightmapOffscreenViewport.RenderTargetUpdateMode = Viewport.UpdateMode.Disabled;
if (HeightMapFrameCount == 0) {
HeightmapOffscreenViewport.RenderTargetUpdateMode = Viewport.UpdateMode.Disabled;
}
HeightMapFrameCount = HeightMapFrameCount > 0 ? HeightMapFrameCount - 1 : 0;
if (TileTypeMapFrameCount == 0) TileTypeOffscreenViewport.RenderTargetUpdateMode = Viewport.UpdateMode.Disabled;
if (TileTypeMapFrameCount == 0) {
TileTypeOffscreenViewport.RenderTargetUpdateMode = Viewport.UpdateMode.Disabled;
}
TileTypeMapFrameCount = TileTypeMapFrameCount > 0 ? TileTypeMapFrameCount - 1 : 0;
PlaneRectMesh.MaterialOverride = null;
}
public void OnTileClicked(HexTile3D tile)
{
public void OnTileClicked(HexTile3D tile) {
EmitSignal("TileClicked", tile);
}
public void OnTileHovered(HexTile3D tile)
{
public void OnTileHovered(HexTile3D tile) {
EmitSignal("TileHovered", tile);
}
}

View File

@ -1,84 +1,68 @@
using Godot;
using System;
using System.Collections.Generic;
using Godot.Collections;
using Godot;
using GodotComponentTest.components;
using GodotComponentTest.entities;
using Array = System.Array;
using NodePair = System.Tuple<Godot.Node, Godot.Node>;
public class InteractionSystem : Node
{
public class InteractionSystem : Node {
private List<InteractionComponent> _activeInteractions;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
public override void _Ready() {
_activeInteractions = new List<InteractionComponent>();
}
public override void _Process(float delta)
{
public override void _Process(float delta) {
base._Process(delta);
List<NodePair> invalidInteractionPairs = new List<NodePair>();
List<InteractionComponent> endedInteractions = new List<InteractionComponent>();
List<InteractionComponent> endedInteractions = new();
foreach (InteractionComponent interaction in _activeInteractions)
{
foreach (InteractionComponent interaction in _activeInteractions) {
Spatial owningEntity = interaction.OwningEntity;
Spatial targetEntity = interaction.TargetEntity;
if (owningEntity == null || owningEntity.IsQueuedForDeletion() || targetEntity == null || targetEntity.IsQueuedForDeletion())
{
if (owningEntity == null || owningEntity.IsQueuedForDeletion() || targetEntity == null ||
targetEntity.IsQueuedForDeletion()) {
interaction.hasStopped = true;
}
if (interaction.hasStopped)
{
if (interaction.hasStopped) {
IInteractionInterface interactableA = owningEntity as IInteractionInterface;
if (interactableA != null)
{
if (interactableA != null) {
interactableA.OnInteractionEnd();
interactableA.InteractionComponent = null;
}
IInteractionInterface interactableB = targetEntity as IInteractionInterface;
if (interactableB != null)
{
if (interactableB != null) {
interactableB.OnInteractionEnd();
interactableB.InteractionComponent = null;
}
endedInteractions.Add(interaction);
}
}
foreach (InteractionComponent interaction in endedInteractions)
{
_activeInteractions.Remove(interaction);
}
}
public void OnStartInteraction(Entity owningEntity, Entity targetEntity)
{
InteractionComponent interactionComponent = new InteractionComponent();
public void OnStartInteraction(Entity owningEntity, Entity targetEntity) {
InteractionComponent interactionComponent = new();
interactionComponent.OwningEntity = owningEntity;
interactionComponent.TargetEntity = targetEntity;
ConnectInteractionSignals(owningEntity, interactionComponent);
ConnectInteractionSignals(targetEntity, interactionComponent);
interactionComponent.EmitSignal("InteractionStart");
_activeInteractions.Add(interactionComponent);
}
private static void ConnectInteractionSignals(Entity entity, InteractionComponent interactionComponent)
{
private static void ConnectInteractionSignals(Entity entity, InteractionComponent interactionComponent) {
IInteractionInterface interactable = entity as IInteractionInterface;
if (interactable != null)
{
if (interactable != null) {
interactable.InteractionComponent = interactionComponent;
interactionComponent.Connect("InteractionStart", entity, nameof(interactable.OnInteractionStart));
interactionComponent.Connect("InteractionEnd", entity, nameof(interactable.OnInteractionEnd));