using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Godot; using GodotComponentTest.utils; /// /// public class NavigationComponent : Spatial { public World World { set; get; } public Vector3 CurrentGoalPositionWorld { get; private set; } = Vector3.Zero; public float CurrentGoalAngleWorld { get; } = 0; public Quat CurrentGoalOrientationWorld { get; private set; } = Quat.Identity; private NavigationPoint _currentGoal; private HexCell[] _path; private List _pathWorldNavigationPoints = new(); private List _planningPathSmoothedWorldNavigationPoints = new(); private List _planningPathWorldNavigationPoints = new(); private List _smoothedPathWorldNavigationPoints = new(); public override void _Ready() { base._Ready(); _pathWorldNavigationPoints = new List(); } public override void _Process(float delta) { Debug.Assert(World != null); } public void PlanSmoothedPath(KinematicBody body, Transform fromTransformWorld, NavigationPoint navigationPoint) { if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position) && navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) { FindPath(body, fromTransformWorld.origin, navigationPoint); } else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) { FindPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition); } 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) { GD.Print("Invalid starting point for FindPath(): returning empty path."); _planningPathWorldNavigationPoints = new List(); _planningPathWorldNavigationPoints.Add(new NavigationPoint(fromPositionWorld)); _planningPathSmoothedWorldNavigationPoints = _planningPathWorldNavigationPoints; return; } var toCell = World.HexGrid.GetHexAt(new Vector2(toPositionWorld.x, toPositionWorld.z)); toCell = World.HexGrid.GetClosestWalkableCell(fromCell, toCell); if (World.HexGrid.GetHexCost(toCell) == 0) { GD.Print("Invalid target point for FindPath(): returning empty path."); _planningPathWorldNavigationPoints = new List(); _planningPathWorldNavigationPoints.Add(new NavigationPoint(fromPositionWorld)); _planningPathSmoothedWorldNavigationPoints = _planningPathWorldNavigationPoints; return; } var path = World.HexGrid.FindPath(fromCell, toCell); // Generate grid navigation points _planningPathWorldNavigationPoints = new List(); foreach (var index in Enumerable.Range(0, path.Count)) { _planningPathWorldNavigationPoints.Add( new NavigationPoint(World.HexGrid.GetHexCenterVec3FromOffset(path[index].OffsetCoords))); } // Ensure the last point coincides with the target position if (_planningPathWorldNavigationPoints.Count > 0 && (_planningPathWorldNavigationPoints.Last().WorldPosition - toPositionWorld).LengthSquared() < 0.5f * 0.5f) { _planningPathWorldNavigationPoints[_planningPathWorldNavigationPoints.Count - 1].WorldPosition = toPositionWorld; } // Perform smoothing _planningPathSmoothedWorldNavigationPoints = SmoothPath(body, _planningPathWorldNavigationPoints); // Ensure starting point is the current position if (_planningPathSmoothedWorldNavigationPoints.Count > 0) { _planningPathSmoothedWorldNavigationPoints[0] = new NavigationPoint(fromPositionWorld); } } public void FindPath(KinematicBody body, Vector3 fromPositionWorld, NavigationPoint navigationPoint) { FindPath(body, fromPositionWorld, navigationPoint.WorldPosition); _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); var fromCell = new HexCell(); fromCell.OffsetCoords = fromPositionOffset; var toCell = new HexCell(); toCell.OffsetCoords = toPositionOffset; _path = fromCell.LineTo(toCell); Debug.Assert(_path.Length > 0); _pathWorldNavigationPoints = new List(); _pathWorldNavigationPoints.Add( new NavigationPoint(World.HexGrid.GetHexCenterVec3FromOffset(fromPositionOffset))); foreach (var 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) { _pathWorldNavigationPoints.RemoveAt(_pathWorldNavigationPoints.Count - 1); } } _pathWorldNavigationPoints.Add(new NavigationPoint(toPositionWorld)); if (_pathWorldNavigationPoints.Count > 2) { _smoothedPathWorldNavigationPoints = SmoothPath(body, _pathWorldNavigationPoints); _pathWorldNavigationPoints = _smoothedPathWorldNavigationPoints; } UpdateCurrentGoal(); } public void PlanGridPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld, Quat toWorldOrientation) { PlanGridPath(body, fromPositionWorld, toPositionWorld); _pathWorldNavigationPoints.Add(new NavigationPoint(toWorldOrientation)); } public void PlanGridPath(KinematicBody body, Transform fromTransformWorld, NavigationPoint navigationPoint) { if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position) && navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) { PlanGridPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition, navigationPoint.WorldOrientation); } else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) { PlanGridPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition); } else { throw new NotImplementedException(); } } public void PlanDirectPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld) { _pathWorldNavigationPoints.Clear(); _pathWorldNavigationPoints.Add(new NavigationPoint(toPositionWorld)); UpdateCurrentGoal(); } public void PlanDirectPath(KinematicBody body, Vector3 fromPositionWorld, Vector3 toPositionWorld, Quat toWorldOrientation) { PlanDirectPath(body, fromPositionWorld, toPositionWorld); _pathWorldNavigationPoints.Add(new NavigationPoint(toWorldOrientation)); } 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) { Spatial colliderSpatial = moveCollision.Collider as Spatial; // GD.Print("Found collision: " + moveCollision.Collider + " (" + colliderSpatial.Name + ")"); return true; } return false; } 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); Vector2 directionPlane = (endPlane - startPlane).Normalized(); Vector2 sidePlane = directionPlane.Rotated(Mathf.Pi * 0.5f); List cells = World.HexGrid.GetCellsForLine(startPlane + directionPlane * radius, endPlane + directionPlane * radius); 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) { return true; } } cells = World.HexGrid.GetCellsForLine(startPlane - sidePlane * radius, endPlane - sidePlane * radius); foreach (HexCell cell in cells) { if (World.HexGrid.GetHexCost(cell) == 0) { return true; } } return false; } public List SmoothPath(KinematicBody body, List navigationPoints) { if (navigationPoints.Count <= 2) { return navigationPoints; } Vector3 bodyGlobalTranslation = body.GlobalTranslation; List smoothedPath = new List(); int startIndex = 0; int endIndex = navigationPoints.Count > 1 ? 1 : 0; smoothedPath.Add(navigationPoints[startIndex]); Vector3 startPoint = navigationPoints[startIndex].WorldPosition; while (endIndex != navigationPoints.Count) { Vector3 endPoint = navigationPoints[endIndex].WorldPosition; 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; } smoothedPath.Add(navigationPoints[endIndex - 1]); startIndex = endIndex - 1; startPoint = navigationPoints[startIndex].WorldPosition; body.GlobalTranslation = startPoint; continue; } if (endIndex == navigationPoints.Count - 1) { break; } endIndex += 1; } smoothedPath.Add(navigationPoints[endIndex]); body.GlobalTranslation = bodyGlobalTranslation; return smoothedPath; } public void PlanDirectPath(KinematicBody body, Transform fromTransformWorld, NavigationPoint navigationPoint) { if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position) && navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) { PlanDirectPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition, navigationPoint.WorldOrientation); } else if (navigationPoint.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) { PlanDirectPath(body, fromTransformWorld.origin, navigationPoint.WorldPosition); } else { throw new NotImplementedException(); } } public void ActivatePlannedPath() { _pathWorldNavigationPoints = _planningPathSmoothedWorldNavigationPoints; UpdateCurrentGoal(); } private void UpdateCurrentGoal() { if (_pathWorldNavigationPoints.Count == 0) { return; } _currentGoal = _pathWorldNavigationPoints[0]; CurrentGoalPositionWorld = _pathWorldNavigationPoints[0].WorldPosition; //_currentGoalOrientationWorld = Vector3.Right.SignedAngleTo(_pathWorldNavigationPoints[0].WorldOrientation); // GD.Print("Navigation Goal: pos " + _currentGoal.WorldPosition + " " + " rot: " + _currentGoal.WorldOrientation + // " flags: " + _currentGoal.Flags + " path length: " + // _pathWorldNavigationPoints.Count); } private void ApplyExistingTransform(Transform worldTransform) { if (_currentGoal.Flags == NavigationPoint.NavigationFlags.Orientation) { CurrentGoalPositionWorld = worldTransform.origin; } else if (_currentGoal.Flags == NavigationPoint.NavigationFlags.Position) { CurrentGoalOrientationWorld = worldTransform.basis.Quat(); } } public void UpdateCurrentGoal(Transform currentTransformWorld) { if (_currentGoal == null) { _currentGoal = new NavigationPoint(currentTransformWorld); } if (_pathWorldNavigationPoints.Count == 0) { CurrentGoalOrientationWorld = currentTransformWorld.basis.Quat(); CurrentGoalPositionWorld = currentTransformWorld.origin; return; } if (_currentGoal.Flags.HasFlag(NavigationPoint.NavigationFlags.Position)) { CurrentGoalPositionWorld = _pathWorldNavigationPoints[0].WorldPosition; } else { CurrentGoalOrientationWorld = currentTransformWorld.basis.Quat(); } if (_currentGoal.Flags.HasFlag(NavigationPoint.NavigationFlags.Orientation)) { CurrentGoalOrientationWorld = _currentGoal.WorldOrientation; } else { CurrentGoalOrientationWorld = currentTransformWorld.basis.Quat(); } // Vector3 currentWorldXAxis = currentTransformWorld.basis.x; // Debug.Assert(Mathf.Abs(currentWorldXAxis.y) < 0.9); // float angle = Mathf.Atan2(currentWorldXAxis.y, currentWorldXAxis.x); // _currentGoalOrientationWorld = Basis.Identity.Rotated(Vector3.Up, angle).Quat(); if (_currentGoal.IsReached(currentTransformWorld)) { _pathWorldNavigationPoints.RemoveAt(0); UpdateCurrentGoal(); ApplyExistingTransform(currentTransformWorld); } if (_pathWorldNavigationPoints.Count == 0) { CurrentGoalOrientationWorld = currentTransformWorld.basis.Quat(); CurrentGoalPositionWorld = currentTransformWorld.origin; } } public bool IsGoalReached() { return _pathWorldNavigationPoints.Count == 0; } public void DebugDraw(Spatial parentNode, DebugGeometry debugGeometry) { Vector3 yOffset = Vector3.Up * 0.1f; debugGeometry.GlobalTransform = Transform.Identity; debugGeometry.Begin(Mesh.PrimitiveType.Lines); Color pinkColor = Colors.Pink; pinkColor.a = 1; debugGeometry.SetColor(pinkColor); debugGeometry.AddVertex(parentNode.GlobalTranslation + yOffset); debugGeometry.SetColor(pinkColor); debugGeometry.AddVertex(CurrentGoalPositionWorld + yOffset); debugGeometry.SetColor(pinkColor); debugGeometry.PushTranslated(CurrentGoalPositionWorld); debugGeometry.AddBox(Vector3.One * 1); debugGeometry.PopTransform(); Vector3 previousPoint = parentNode.GlobalTranslation; foreach (NavigationPoint point in _pathWorldNavigationPoints) { debugGeometry.AddVertex(previousPoint + yOffset); debugGeometry.AddVertex(point.WorldPosition + yOffset); previousPoint = point.WorldPosition; } previousPoint = parentNode.GlobalTranslation; foreach (NavigationPoint point in _smoothedPathWorldNavigationPoints) { debugGeometry.SetColor(new Color(0, 0, 1)); debugGeometry.AddVertex(previousPoint + yOffset); debugGeometry.AddVertex(point.WorldPosition + yOffset); previousPoint = point.WorldPosition; } previousPoint = parentNode.GlobalTranslation; foreach (NavigationPoint point in _planningPathWorldNavigationPoints) { debugGeometry.SetColor(new Color(1, 0, 1)); debugGeometry.AddVertex(previousPoint + yOffset); debugGeometry.AddVertex(point.WorldPosition + yOffset); previousPoint = point.WorldPosition; } previousPoint = parentNode.GlobalTranslation; foreach (NavigationPoint point in _planningPathSmoothedWorldNavigationPoints) { debugGeometry.SetColor(new Color(1, 1, 0)); debugGeometry.AddVertex(previousPoint + yOffset); debugGeometry.AddVertex(point.WorldPosition + yOffset); previousPoint = point.WorldPosition; } debugGeometry.End(); } public class NavigationPoint { [Flags] public enum NavigationFlags { Position = 1, Orientation = 2 } public readonly NavigationFlags Flags; public Quat WorldOrientation = Quat.Identity; public Vector3 WorldPosition = Vector3.Zero; public NavigationPoint(Vector3 worldPosition) { WorldPosition = worldPosition; Flags = NavigationFlags.Position; } public NavigationPoint(Quat worldOrientation) { WorldOrientation = worldOrientation; Flags = NavigationFlags.Orientation; } public NavigationPoint(Transform worldTransform) { WorldPosition = worldTransform.origin; WorldOrientation = worldTransform.basis.Quat(); Flags = NavigationFlags.Position | NavigationFlags.Orientation; } public bool IsReached(Transform worldTransform) { var goalReached = false; var positionError = new Vector2(WorldPosition.x - worldTransform.origin.x, WorldPosition.z - worldTransform.origin.z); var positionErrorSquared = positionError.LengthSquared(); worldTransform.basis.Quat(); var orientationError = Mathf.Abs(worldTransform.basis.Quat().AngleTo(WorldOrientation)); if (Flags.HasFlag(NavigationFlags.Position) && Flags.HasFlag(NavigationFlags.Orientation) && positionErrorSquared < Globals.EpsPositionSquared && orientationError < Globals.EpsRadians) goalReached = true; else if (Flags == NavigationFlags.Position && positionErrorSquared < Globals.EpsPositionSquared) goalReached = true; else if (Flags == NavigationFlags.Orientation && orientationError < Globals.EpsRadians) goalReached = true; return goalReached; } } }