Refactor AI movement (#1222)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
@@ -1,297 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Content.Server.GameObjects.Components.Access;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding;
|
||||
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
|
||||
using Content.Server.GameObjects.EntitySystems.JobQueues;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Timer = Robust.Shared.Timers.Timer;
|
||||
|
||||
namespace Content.Server.AI.Operators.Movement
|
||||
{
|
||||
public abstract class BaseMover : AiOperator
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked every time we move across a tile
|
||||
/// </summary>
|
||||
public event Action MovedATile;
|
||||
|
||||
/// <summary>
|
||||
/// How close the pathfinder needs to get before returning a route
|
||||
/// Set at 1.42f just in case there's rounding and diagonally adjacent tiles aren't counted.
|
||||
///
|
||||
/// </summary>
|
||||
public float PathfindingProximity { get; set; } = 1.42f;
|
||||
protected Queue<TileRef> Route = new Queue<TileRef>();
|
||||
/// <summary>
|
||||
/// The final spot we're trying to get to
|
||||
/// </summary>
|
||||
protected GridCoordinates TargetGrid;
|
||||
/// <summary>
|
||||
/// As the pathfinder is tilebased we'll move to each tile's grid.
|
||||
/// </summary>
|
||||
protected GridCoordinates NextGrid;
|
||||
private const float TileTolerance = 0.2f;
|
||||
|
||||
// Stuck checkers
|
||||
/// <summary>
|
||||
/// How long we're stuck in general before trying to unstuck
|
||||
/// </summary>
|
||||
private float _stuckTimerRemaining = 0.5f;
|
||||
private GridCoordinates _ourLastPosition;
|
||||
|
||||
// Anti-stuck measures. See the AntiStuck() method for more details
|
||||
private bool _tryingAntiStuck;
|
||||
public bool IsStuck;
|
||||
private AntiStuckMethod _antiStuckMethod = AntiStuckMethod.Angle;
|
||||
private Angle _addedAngle = Angle.Zero;
|
||||
public event Action Stuck;
|
||||
private int _antiStuckAttempts = 0;
|
||||
|
||||
private CancellationTokenSource _routeCancelToken;
|
||||
protected Job<Queue<TileRef>> RouteJob;
|
||||
private IMapManager _mapManager;
|
||||
private PathfindingSystem _pathfinder;
|
||||
private AiControllerComponent _controller;
|
||||
|
||||
// Input
|
||||
protected IEntity Owner;
|
||||
|
||||
protected void Setup(IEntity owner)
|
||||
{
|
||||
Owner = owner;
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
_pathfinder = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<PathfindingSystem>();
|
||||
if (!Owner.TryGetComponent(out AiControllerComponent controllerComponent))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_controller = controllerComponent;
|
||||
}
|
||||
|
||||
protected void NextTile()
|
||||
{
|
||||
MovedATile?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will move the AI towards the next position
|
||||
/// </summary>
|
||||
/// <returns>true if movement to be done</returns>
|
||||
protected bool TryMove()
|
||||
{
|
||||
// Use collidable just so we don't get stuck on corners as much
|
||||
// var targetDiff = NextGrid.Position - _ownerCollidable.WorldAABB.Center;
|
||||
var targetDiff = NextGrid.Position - Owner.Transform.GridPosition.Position;
|
||||
|
||||
// Check distance
|
||||
if (targetDiff.Length < TileTolerance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Move towards it
|
||||
if (_controller == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_controller.VelocityDir = _addedAngle.RotateVec(targetDiff).Normalized;
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will try and get around obstacles if stuck
|
||||
/// </summary>
|
||||
protected void AntiStuck(float frameTime)
|
||||
{
|
||||
// TODO: More work because these are sketchy af
|
||||
// TODO: Check if a wall was spawned in front of us and then immediately dump route if it was
|
||||
|
||||
// First check if we're still in a stuck state from last frame
|
||||
if (IsStuck && !_tryingAntiStuck)
|
||||
{
|
||||
switch (_antiStuckMethod)
|
||||
{
|
||||
case AntiStuckMethod.None:
|
||||
break;
|
||||
case AntiStuckMethod.Jiggle:
|
||||
var randomRange = IoCManager.Resolve<IRobustRandom>().Next(0, 359);
|
||||
var angle = Angle.FromDegrees(randomRange);
|
||||
Owner.TryGetComponent(out AiControllerComponent mover);
|
||||
mover.VelocityDir = angle.ToVec().Normalized;
|
||||
|
||||
break;
|
||||
case AntiStuckMethod.PhaseThrough:
|
||||
if (Owner.TryGetComponent(out CollidableComponent collidableComponent))
|
||||
{
|
||||
// TODO Fix this because they are yeeting themselves when they charge
|
||||
// TODO: If something updates this this will fuck it
|
||||
collidableComponent.CanCollide = false;
|
||||
|
||||
Timer.Spawn(100, () =>
|
||||
{
|
||||
if (!collidableComponent.CanCollide)
|
||||
{
|
||||
collidableComponent.CanCollide = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case AntiStuckMethod.Teleport:
|
||||
Owner.Transform.DetachParent();
|
||||
Owner.Transform.GridPosition = NextGrid;
|
||||
break;
|
||||
case AntiStuckMethod.ReRoute:
|
||||
GetRoute();
|
||||
break;
|
||||
case AntiStuckMethod.Angle:
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
_addedAngle = new Angle(random.Next(-60, 60));
|
||||
IsStuck = false;
|
||||
Timer.Spawn(100, () =>
|
||||
{
|
||||
_addedAngle = Angle.Zero;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
_stuckTimerRemaining -= frameTime;
|
||||
|
||||
// Stuck check cooldown
|
||||
if (_stuckTimerRemaining > 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tryingAntiStuck = false;
|
||||
_stuckTimerRemaining = 0.5f;
|
||||
|
||||
// Are we actually stuck
|
||||
if ((_ourLastPosition.Position - Owner.Transform.GridPosition.Position).Length < TileTolerance)
|
||||
{
|
||||
_antiStuckAttempts++;
|
||||
|
||||
// Maybe it's just 1 tile that's borked so try next 1?
|
||||
if (_antiStuckAttempts >= 2 && _antiStuckAttempts < 5 && Route.Count > 1)
|
||||
{
|
||||
var nextTile = Route.Dequeue();
|
||||
NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_antiStuckAttempts >= 5 || Route.Count == 0)
|
||||
{
|
||||
Logger.DebugS("ai", $"{Owner} is stuck at {Owner.Transform.GridPosition}, trying new route");
|
||||
_antiStuckAttempts = 0;
|
||||
IsStuck = false;
|
||||
_ourLastPosition = Owner.Transform.GridPosition;
|
||||
GetRoute();
|
||||
return;
|
||||
}
|
||||
Stuck?.Invoke();
|
||||
IsStuck = true;
|
||||
return;
|
||||
}
|
||||
|
||||
IsStuck = false;
|
||||
|
||||
_ourLastPosition = Owner.Transform.GridPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells us we don't need to keep moving and resets everything
|
||||
/// </summary>
|
||||
public void HaveArrived()
|
||||
{
|
||||
_routeCancelToken?.Cancel(); // oh thank god no more pathfinding
|
||||
Route.Clear();
|
||||
if (_controller == null) return;
|
||||
_controller.VelocityDir = Vector2.Zero;
|
||||
}
|
||||
|
||||
protected void GetRoute()
|
||||
{
|
||||
_routeCancelToken?.Cancel();
|
||||
_routeCancelToken = new CancellationTokenSource();
|
||||
Route.Clear();
|
||||
|
||||
int collisionMask;
|
||||
if (!Owner.TryGetComponent(out CollidableComponent collidableComponent))
|
||||
{
|
||||
collisionMask = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
collisionMask = collidableComponent.CollisionMask;
|
||||
}
|
||||
|
||||
var startGrid = _mapManager.GetGrid(Owner.Transform.GridID).GetTileRef(Owner.Transform.GridPosition);
|
||||
var endGrid = _mapManager.GetGrid(TargetGrid.GridID).GetTileRef(TargetGrid);;
|
||||
var access = AccessReader.FindAccessTags(Owner);
|
||||
|
||||
RouteJob = _pathfinder.RequestPath(new PathfindingArgs(
|
||||
Owner.Uid,
|
||||
access,
|
||||
collisionMask,
|
||||
startGrid,
|
||||
endGrid,
|
||||
PathfindingProximity
|
||||
), _routeCancelToken.Token);
|
||||
}
|
||||
|
||||
protected void ReceivedRoute()
|
||||
{
|
||||
Route = RouteJob.Result;
|
||||
RouteJob = null;
|
||||
|
||||
if (Route == null)
|
||||
{
|
||||
Route = new Queue<TileRef>();
|
||||
// Couldn't find a route to target
|
||||
return;
|
||||
}
|
||||
|
||||
// Because the entity may be half on 2 tiles we'll just cut out the first tile.
|
||||
// This may not be the best solution but sometimes if the AI is chasing for example it will
|
||||
// stutter backwards to the first tile again.
|
||||
Route.Dequeue();
|
||||
|
||||
var nextTile = Route.Peek();
|
||||
NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
|
||||
}
|
||||
|
||||
public override Outcome Execute(float frameTime)
|
||||
{
|
||||
if (RouteJob != null && RouteJob.Status == JobStatus.Finished)
|
||||
{
|
||||
ReceivedRoute();
|
||||
}
|
||||
|
||||
return !ActionBlockerSystem.CanMove(Owner) ? Outcome.Failed : Outcome.Continuing;
|
||||
}
|
||||
}
|
||||
|
||||
public enum AntiStuckMethod
|
||||
{
|
||||
None,
|
||||
ReRoute,
|
||||
Jiggle, // Just pick a random direction for a bit and hope for the best
|
||||
Teleport, // The Half-Life 2 method
|
||||
PhaseThrough, // Just makes it non-collidable
|
||||
Angle, // Add a different angle for a bit
|
||||
}
|
||||
}
|
||||
@@ -1,142 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.EntitySystems.JobQueues;
|
||||
using System;
|
||||
using System.IO;
|
||||
using Content.Server.GameObjects.EntitySystems.AI.Steering;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.AI.Operators.Movement
|
||||
{
|
||||
public sealed class MoveToEntityOperator : BaseMover
|
||||
public sealed class MoveToEntityOperator : AiOperator
|
||||
{
|
||||
// Instance
|
||||
private GridCoordinates _lastTargetPosition;
|
||||
private IMapManager _mapManager;
|
||||
// TODO: This and steering need to support InRangeUnobstructed now
|
||||
private readonly IEntity _owner;
|
||||
private EntityTargetSteeringRequest _request;
|
||||
private readonly IEntity _target;
|
||||
// For now we'll just get as close as we can because we're not doing LOS checks to be able to pick up at the max interaction range
|
||||
public float ArrivalDistance { get; }
|
||||
public float PathfindingProximity { get; }
|
||||
|
||||
// Input
|
||||
public IEntity Target { get; }
|
||||
public float DesiredRange { get; set; }
|
||||
|
||||
public MoveToEntityOperator(IEntity owner, IEntity target, float desiredRange = 1.5f)
|
||||
public MoveToEntityOperator(IEntity owner, IEntity target, float arrivalDistance = 1.0f, float pathfindingProximity = 1.5f)
|
||||
{
|
||||
Setup(owner);
|
||||
Target = target;
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
DesiredRange = desiredRange;
|
||||
_owner = owner;
|
||||
_target = target;
|
||||
ArrivalDistance = arrivalDistance;
|
||||
PathfindingProximity = pathfindingProximity;
|
||||
}
|
||||
|
||||
public override bool TryStartup()
|
||||
{
|
||||
if (!base.TryStartup())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var steering = EntitySystem.Get<AiSteeringSystem>();
|
||||
_request = new EntityTargetSteeringRequest(_target, ArrivalDistance, PathfindingProximity);
|
||||
steering.Register(_owner, _request);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Shutdown(Outcome outcome)
|
||||
{
|
||||
base.Shutdown(outcome);
|
||||
var steering = EntitySystem.Get<AiSteeringSystem>();
|
||||
steering.Unregister(_owner);
|
||||
}
|
||||
|
||||
public override Outcome Execute(float frameTime)
|
||||
{
|
||||
var baseOutcome = base.Execute(frameTime);
|
||||
// TODO: Given this is probably the most common operator whatever speed boosts you can do here will be gucci
|
||||
// Could also look at running it every other tick.
|
||||
|
||||
if (baseOutcome == Outcome.Failed ||
|
||||
Target == null ||
|
||||
Target.Deleted ||
|
||||
Target.Transform.GridID != Owner.Transform.GridID)
|
||||
switch (_request.Status)
|
||||
{
|
||||
HaveArrived();
|
||||
return Outcome.Failed;
|
||||
}
|
||||
|
||||
if (RouteJob != null)
|
||||
{
|
||||
if (RouteJob.Status != JobStatus.Finished)
|
||||
{
|
||||
case SteeringStatus.Pending:
|
||||
DebugTools.Assert(EntitySystem.Get<AiSteeringSystem>().IsRegistered(_owner));
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
ReceivedRoute();
|
||||
return Route.Count == 0 ? Outcome.Failed : Outcome.Continuing;
|
||||
}
|
||||
|
||||
var targetRange = (Target.Transform.GridPosition.Position - Owner.Transform.GridPosition.Position).Length;
|
||||
|
||||
// If they move near us
|
||||
if (targetRange <= DesiredRange)
|
||||
{
|
||||
HaveArrived();
|
||||
return Outcome.Success;
|
||||
}
|
||||
|
||||
// If the target's moved we may need to re-route.
|
||||
// First we'll check if they're near another tile on the existing route and if so
|
||||
// we can trim up until that point.
|
||||
if (_lastTargetPosition != default &&
|
||||
(Target.Transform.GridPosition.Position - _lastTargetPosition.Position).Length > 1.5f)
|
||||
{
|
||||
var success = false;
|
||||
// Technically it should be Route.Count - 1 but if the route's empty it'll throw
|
||||
var newRoute = new Queue<TileRef>(Route.Count);
|
||||
|
||||
for (var i = 0; i < Route.Count; i++)
|
||||
{
|
||||
var tile = Route.Dequeue();
|
||||
newRoute.Enqueue(tile);
|
||||
var tileGrid = _mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
|
||||
|
||||
// Don't use DesiredRange here or above in case it's smaller than a tile;
|
||||
// when we get close we run straight at them anyway so it shooouullddd be okay...
|
||||
if ((Target.Transform.GridPosition.Position - tileGrid.Position).Length < 1.5f)
|
||||
{
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
Route = newRoute;
|
||||
_lastTargetPosition = Target.Transform.GridPosition;
|
||||
TargetGrid = Target.Transform.GridPosition;
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
|
||||
_lastTargetPosition = default;
|
||||
}
|
||||
|
||||
// If they move too far or no route
|
||||
if (_lastTargetPosition == default)
|
||||
{
|
||||
// If they're further we could try pathfinding from the furthest tile potentially?
|
||||
_lastTargetPosition = Target.Transform.GridPosition;
|
||||
TargetGrid = Target.Transform.GridPosition;
|
||||
GetRoute();
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
|
||||
AntiStuck(frameTime);
|
||||
|
||||
if (IsStuck)
|
||||
{
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
|
||||
if (TryMove())
|
||||
{
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
|
||||
// If we're really close just try bee-lining it?
|
||||
if (Route.Count == 0)
|
||||
{
|
||||
if (targetRange < 1.9f)
|
||||
{
|
||||
// TODO: If they have a phat hitbox they could block us
|
||||
NextGrid = TargetGrid;
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
if (targetRange > DesiredRange)
|
||||
{
|
||||
HaveArrived();
|
||||
case SteeringStatus.NoPath:
|
||||
return Outcome.Failed;
|
||||
}
|
||||
case SteeringStatus.Arrived:
|
||||
return Outcome.Success;
|
||||
case SteeringStatus.Moving:
|
||||
DebugTools.Assert(EntitySystem.Get<AiSteeringSystem>().IsRegistered(_owner));
|
||||
return Outcome.Continuing;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
var nextTile = Route.Dequeue();
|
||||
NextTile();
|
||||
NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,63 @@
|
||||
using Content.Server.GameObjects.EntitySystems.JobQueues;
|
||||
using System;
|
||||
using Content.Server.GameObjects.EntitySystems.AI.Steering;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.AI.Operators.Movement
|
||||
{
|
||||
public class MoveToGridOperator : BaseMover
|
||||
public sealed class MoveToGridOperator : AiOperator
|
||||
{
|
||||
private IMapManager _mapManager;
|
||||
private float _desiredRange;
|
||||
private readonly IEntity _owner;
|
||||
private GridTargetSteeringRequest _request;
|
||||
private readonly GridCoordinates _target;
|
||||
public float DesiredRange { get; set; }
|
||||
|
||||
public MoveToGridOperator(
|
||||
IEntity owner,
|
||||
GridCoordinates gridPosition,
|
||||
float desiredRange = 1.5f)
|
||||
public MoveToGridOperator(IEntity owner, GridCoordinates target, float desiredRange = 1.5f)
|
||||
{
|
||||
Setup(owner);
|
||||
TargetGrid = gridPosition;
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
PathfindingProximity = 0.2f; // Accept no substitutes
|
||||
_desiredRange = desiredRange;
|
||||
_owner = owner;
|
||||
_target = target;
|
||||
DesiredRange = desiredRange;
|
||||
}
|
||||
|
||||
public void UpdateTarget(GridCoordinates newTarget)
|
||||
public override bool TryStartup()
|
||||
{
|
||||
TargetGrid = newTarget;
|
||||
HaveArrived();
|
||||
GetRoute();
|
||||
if (!base.TryStartup())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var steering = EntitySystem.Get<AiSteeringSystem>();
|
||||
_request = new GridTargetSteeringRequest(_target, DesiredRange);
|
||||
steering.Register(_owner, _request);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Shutdown(Outcome outcome)
|
||||
{
|
||||
base.Shutdown(outcome);
|
||||
var steering = EntitySystem.Get<AiSteeringSystem>();
|
||||
steering.Unregister(_owner);
|
||||
}
|
||||
|
||||
public override Outcome Execute(float frameTime)
|
||||
{
|
||||
var baseOutcome = base.Execute(frameTime);
|
||||
|
||||
if (baseOutcome == Outcome.Failed ||
|
||||
TargetGrid.GridID != Owner.Transform.GridID)
|
||||
switch (_request.Status)
|
||||
{
|
||||
HaveArrived();
|
||||
return Outcome.Failed;
|
||||
}
|
||||
|
||||
if (RouteJob != null)
|
||||
{
|
||||
if (RouteJob.Status != JobStatus.Finished)
|
||||
{
|
||||
case SteeringStatus.Pending:
|
||||
DebugTools.Assert(EntitySystem.Get<AiSteeringSystem>().IsRegistered(_owner));
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
ReceivedRoute();
|
||||
return Route.Count == 0 ? Outcome.Failed : Outcome.Continuing;
|
||||
case SteeringStatus.NoPath:
|
||||
return Outcome.Failed;
|
||||
case SteeringStatus.Arrived:
|
||||
return Outcome.Success;
|
||||
case SteeringStatus.Moving:
|
||||
DebugTools.Assert(EntitySystem.Get<AiSteeringSystem>().IsRegistered(_owner));
|
||||
return Outcome.Continuing;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
var targetRange = (TargetGrid.Position - Owner.Transform.GridPosition.Position).Length;
|
||||
|
||||
// We there
|
||||
if (targetRange <= _desiredRange)
|
||||
{
|
||||
HaveArrived();
|
||||
return Outcome.Success;
|
||||
}
|
||||
|
||||
// No route
|
||||
if (Route.Count == 0 && RouteJob == null)
|
||||
{
|
||||
GetRoute();
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
|
||||
AntiStuck(frameTime);
|
||||
|
||||
if (IsStuck)
|
||||
{
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
|
||||
if (TryMove())
|
||||
{
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
|
||||
if (Route.Count == 0 && targetRange > 1.5f)
|
||||
{
|
||||
HaveArrived();
|
||||
return Outcome.Failed;
|
||||
}
|
||||
|
||||
var nextTile = Route.Dequeue();
|
||||
NextTile();
|
||||
NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user