Files
OpenRA/OpenRA.Mods.Common/Activities/Move/Move.cs
reaperrr 27ddae3df9 Fix Move jumpy-ness on MovePart transitions
There were 2 issues at work here, both when progress
would overshoot Distance (which is the usual case,
rather than the exception):
- the overshot progress was passed on by MoveFirstHalf, however
  OnComplete would make the next MovePart start ticking the
  same tick on which the old MovePart reached Distance,
  but move by carryoverProgress +(!!!) terrain speed instead of moving
  by just the left-over carryoverProgress.
- MoveSecondHalf would not pass any overshot progress to the
  next MoveFirstHalf queued by parent Move, leading to
  the next MoveFirstHalf performing a full-speed move the same tick
  MoveSecondHalf finished its last move.
2021-04-15 18:03:42 +02:00

570 lines
18 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
public class Move : Activity
{
static readonly List<CPos> NoPath = new List<CPos>();
readonly Mobile mobile;
readonly WDist nearEnough;
readonly Func<BlockedByActor, List<CPos>> getPath;
readonly Actor ignoreActor;
readonly Color? targetLineColor;
static readonly BlockedByActor[] PathSearchOrder =
{
BlockedByActor.All,
BlockedByActor.Immovable,
BlockedByActor.Stationary,
BlockedByActor.None
};
int carryoverProgress;
List<CPos> path;
CPos? destination;
// For dealing with blockers
bool hasWaited;
int waitTicksRemaining;
// To work around queued activity issues while minimizing changes to legacy behaviour
bool evaluateNearestMovableCell;
// Scriptable move order
// Ignores lane bias and nearby units
public Move(Actor self, CPos destination, Color? targetLineColor = null)
{
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
mobile = (Mobile)self.OccupiesSpace;
getPath = check =>
{
List<CPos> path;
using (var search =
PathSearch.FromPoint(self.World, mobile.Locomotor, self, mobile.ToCell, destination, check)
.WithoutLaneBias())
path = mobile.Pathfinder.FindPath(search);
return path;
};
this.destination = destination;
this.targetLineColor = targetLineColor;
nearEnough = WDist.Zero;
}
public Move(Actor self, CPos destination, WDist nearEnough, Actor ignoreActor = null, bool evaluateNearestMovableCell = false,
Color? targetLineColor = null)
{
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
mobile = (Mobile)self.OccupiesSpace;
getPath = check =>
{
if (!this.destination.HasValue)
return NoPath;
return mobile.Pathfinder.FindUnitPath(mobile.ToCell, this.destination.Value, self, ignoreActor, check);
};
// Note: Will be recalculated from OnFirstRun if evaluateNearestMovableCell is true
this.destination = destination;
this.nearEnough = nearEnough;
this.ignoreActor = ignoreActor;
this.evaluateNearestMovableCell = evaluateNearestMovableCell;
this.targetLineColor = targetLineColor;
}
public Move(Actor self, CPos destination, SubCell subCell, WDist nearEnough, Color? targetLineColor = null)
{
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
mobile = (Mobile)self.OccupiesSpace;
getPath = check => mobile.Pathfinder.FindUnitPathToRange(
mobile.FromCell, subCell, self.World.Map.CenterOfSubCell(destination, subCell), nearEnough, self, check);
this.destination = destination;
this.nearEnough = nearEnough;
this.targetLineColor = targetLineColor;
}
public Move(Actor self, Target target, WDist range, Color? targetLineColor = null)
{
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
mobile = (Mobile)self.OccupiesSpace;
getPath = check =>
{
if (!target.IsValidFor(self))
return NoPath;
return mobile.Pathfinder.FindUnitPathToRange(
mobile.ToCell, mobile.ToSubCell, target.CenterPosition, range, self, check);
};
destination = null;
nearEnough = range;
this.targetLineColor = targetLineColor;
}
public Move(Actor self, Func<BlockedByActor, List<CPos>> getPath, Color? targetLineColor = null)
{
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
mobile = (Mobile)self.OccupiesSpace;
this.getPath = getPath;
destination = null;
nearEnough = WDist.Zero;
this.targetLineColor = targetLineColor;
}
static int HashList<T>(List<T> xs)
{
var hash = 0;
var n = 0;
foreach (var x in xs)
hash += n++ * x.GetHashCode();
return hash;
}
List<CPos> EvalPath(BlockedByActor check)
{
var path = getPath(check).TakeWhile(a => a != mobile.ToCell).ToList();
return path;
}
protected override void OnFirstRun(Actor self)
{
if (evaluateNearestMovableCell && destination.HasValue)
{
var movableDestination = mobile.NearestMoveableCell(destination.Value);
destination = mobile.CanEnterCell(movableDestination, check: BlockedByActor.Immovable) ? movableDestination : (CPos?)null;
}
// TODO: Change this to BlockedByActor.Stationary after improving the local avoidance behaviour
foreach (var check in PathSearchOrder)
{
path = EvalPath(check);
if (path.Count > 0)
return;
}
}
public override bool Tick(Actor self)
{
mobile.TurnToMove = false;
if (IsCanceling && mobile.CanStayInCell(mobile.ToCell))
{
path?.Clear();
return true;
}
if (mobile.IsTraitDisabled || mobile.IsTraitPaused)
return false;
if (destination == mobile.ToCell)
return true;
if (path.Count == 0)
{
destination = mobile.ToCell;
return false;
}
destination = path[0];
var nextCell = PopPath(self);
if (nextCell == null)
return false;
var firstFacing = self.World.Map.FacingBetween(mobile.FromCell, nextCell.Value.Cell, mobile.Facing);
if (firstFacing != mobile.Facing)
{
path.Add(nextCell.Value.Cell);
QueueChild(new Turn(self, firstFacing));
mobile.TurnToMove = true;
return false;
}
mobile.SetLocation(mobile.FromCell, mobile.FromSubCell, nextCell.Value.Cell, nextCell.Value.SubCell);
var map = self.World.Map;
var from = (mobile.FromCell.Layer == 0 ? map.CenterOfCell(mobile.FromCell) :
self.World.GetCustomMovementLayers()[mobile.FromCell.Layer].CenterOfCell(mobile.FromCell)) +
map.Grid.OffsetOfSubCell(mobile.FromSubCell);
var to = Util.BetweenCells(self.World, mobile.FromCell, mobile.ToCell) +
(map.Grid.OffsetOfSubCell(mobile.FromSubCell) + map.Grid.OffsetOfSubCell(mobile.ToSubCell)) / 2;
QueueChild(new MoveFirstHalf(this, from, to, mobile.Facing, mobile.Facing, carryoverProgress));
carryoverProgress = 0;
return false;
}
(CPos Cell, SubCell SubCell)? PopPath(Actor self)
{
if (path.Count == 0)
return null;
var nextCell = path[path.Count - 1];
// Something else might have moved us, so the path is no longer valid.
if (!Util.AreAdjacentCells(mobile.ToCell, nextCell))
{
path = EvalPath(BlockedByActor.Immovable);
return null;
}
var containsTemporaryBlocker = WorldUtils.ContainsTemporaryBlocker(self.World, nextCell, self);
// Next cell in the move is blocked by another actor
if (containsTemporaryBlocker || !mobile.CanEnterCell(nextCell, ignoreActor))
{
// Are we close enough?
var cellRange = nearEnough.Length / 1024;
if (!containsTemporaryBlocker && (mobile.ToCell - destination.Value).LengthSquared <= cellRange * cellRange && mobile.CanStayInCell(mobile.ToCell))
{
// Apply some simple checks to avoid giving up in cases where we can be confident that
// nudging/waiting/repathing should produce better results.
// Avoid fighting over the destination cell
if (path.Count < 2)
{
path.Clear();
return null;
}
// We can reasonably assume that the blocker is friendly and has a similar locomotor type.
// If there is a free cell next to the blocker that is a similar or closer distance to the
// destination then we can probably nudge or path around it.
var blockerDistSq = (nextCell - destination.Value).LengthSquared;
var nudgeOrRepath = CVec.Directions
.Select(d => nextCell + d)
.Any(c => c != self.Location && (c - destination.Value).LengthSquared <= blockerDistSq && mobile.CanEnterCell(c, ignoreActor));
if (!nudgeOrRepath)
{
path.Clear();
return null;
}
}
// There is no point in waiting for the other actor to move if it is incapable of moving.
if (!mobile.CanEnterCell(nextCell, ignoreActor, BlockedByActor.Immovable))
{
path = EvalPath(BlockedByActor.Immovable);
return null;
}
// See if they will move
self.NotifyBlocker(nextCell);
// Wait a bit to see if they leave
if (!hasWaited)
{
waitTicksRemaining = mobile.Info.LocomotorInfo.WaitAverage;
hasWaited = true;
return null;
}
if (--waitTicksRemaining >= 0)
return null;
hasWaited = false;
// If the blocking actors are already leaving, wait a little longer instead of repathing
if (CellIsEvacuating(self, nextCell))
return null;
// Calculate a new path
mobile.RemoveInfluence();
var newPath = EvalPath(BlockedByActor.All);
mobile.AddInfluence();
if (newPath.Count != 0)
{
path = newPath;
var newCell = path[path.Count - 1];
path.RemoveAt(path.Count - 1);
return (newCell, mobile.GetAvailableSubCell(nextCell, mobile.FromSubCell, ignoreActor));
}
else if (mobile.IsBlocking)
{
// If there is no way around the blocker and blocker will not move and we are blocking others, back up to let others pass.
var newCell = mobile.GetAdjacentCell(nextCell);
if (newCell != null)
{
if ((nextCell - newCell).Value.LengthSquared > 2)
path.Add(mobile.ToCell);
return (newCell.Value, mobile.GetAvailableSubCell(newCell.Value, mobile.FromSubCell, ignoreActor));
}
}
return null;
}
hasWaited = false;
path.RemoveAt(path.Count - 1);
return (nextCell, mobile.GetAvailableSubCell(nextCell, mobile.FromSubCell, ignoreActor));
}
protected override void OnLastRun(Actor self)
{
path = null;
}
bool CellIsEvacuating(Actor self, CPos cell)
{
foreach (var actor in self.World.ActorMap.GetActorsAt(cell))
{
var move = actor.OccupiesSpace as Mobile;
if (move == null || !move.IsTraitEnabled() || !move.IsLeaving())
return false;
}
return true;
}
public override void Cancel(Actor self, bool keepQueue = false)
{
Cancel(self, keepQueue, false);
}
public void Cancel(Actor self, bool keepQueue, bool forceClearPath)
{
// We need to clear the path here in order to prevent MovePart queueing new instances of itself
// when the unit is making a turn.
if (path != null && (forceClearPath || mobile.CanStayInCell(mobile.ToCell)))
path.Clear();
base.Cancel(self, keepQueue);
}
public override IEnumerable<Target> GetTargets(Actor self)
{
if (path != null)
return Enumerable.Reverse(path).Select(c => Target.FromCell(self.World, c));
if (destination != null)
return new Target[] { Target.FromCell(self.World, destination.Value) };
return Target.None;
}
public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self)
{
// destination might be initialized with null, but will be set in a subsequent tick
if (targetLineColor != null && destination != null)
yield return new TargetLineNode(Target.FromCell(self.World, destination.Value), targetLineColor.Value);
}
abstract class MovePart : Activity
{
protected readonly Move Move;
protected readonly WPos From, To;
protected readonly WAngle FromFacing, ToFacing;
protected readonly bool EnableArc;
protected readonly WPos ArcCenter;
protected readonly int ArcFromLength;
protected readonly WAngle ArcFromAngle;
protected readonly int ArcToLength;
protected readonly WAngle ArcToAngle;
protected readonly int Distance;
protected int progress;
protected bool firstTick = true;
public MovePart(Move move, WPos from, WPos to, WAngle fromFacing, WAngle toFacing, int carryoverProgress)
{
Move = move;
From = from;
To = to;
FromFacing = fromFacing;
ToFacing = toFacing;
progress = carryoverProgress;
Distance = (to - from).Length;
IsInterruptible = false; // See comments in Move.Cancel()
// Calculate an elliptical arc that joins from and to
var delta = (fromFacing - toFacing).Angle;
if (delta != 0 && delta != 512)
{
// The center of rotation is where the normal vectors cross
var u = new WVec(1024, 0, 0).Rotate(WRot.FromYaw(fromFacing));
var v = new WVec(1024, 0, 0).Rotate(WRot.FromYaw(toFacing));
// Make sure that u and v aren't parallel, which may happen due to rounding
// in WVec.Rotate if delta is close but not necessarily equal to 0 or 512
if (v.X * u.Y != v.Y * u.X)
{
var w = from - to;
var s = (v.Y * w.X - v.X * w.Y) * 1024 / (v.X * u.Y - v.Y * u.X);
var x = from.X + s * u.X / 1024;
var y = from.Y + s * u.Y / 1024;
ArcCenter = new WPos(x, y, 0);
ArcFromLength = (ArcCenter - from).HorizontalLength;
ArcFromAngle = (ArcCenter - from).Yaw;
ArcToLength = (ArcCenter - to).HorizontalLength;
ArcToAngle = (ArcCenter - to).Yaw;
EnableArc = true;
}
}
}
public override bool Tick(Actor self)
{
var mobile = Move.mobile;
// Having non-zero progress in the first tick means that this MovePart is following on from
// a previous MovePart that has just completed during the same tick. In this case, we want to
// apply the carried over progress but not evaluate a full new step until the next tick.
if (!firstTick || progress == 0)
progress += mobile.MovementSpeedForCell(self, mobile.ToCell);
firstTick = false;
if (progress >= Distance)
{
mobile.SetCenterPosition(self, To);
mobile.Facing = ToFacing;
Queue(OnComplete(self, mobile, Move));
return true;
}
WPos pos;
if (EnableArc)
{
var angle = WAngle.Lerp(ArcFromAngle, ArcToAngle, progress, Distance);
var length = int2.Lerp(ArcFromLength, ArcToLength, progress, Distance);
var height = int2.Lerp(From.Z, To.Z, progress, Distance);
pos = ArcCenter + new WVec(0, length, height).Rotate(WRot.FromYaw(angle));
}
else
pos = WPos.Lerp(From, To, progress, Distance);
if (self.Location.Layer == 0)
pos -= new WVec(WDist.Zero, WDist.Zero, self.World.Map.DistanceAboveTerrain(pos));
mobile.SetCenterPosition(self, pos);
mobile.Facing = WAngle.Lerp(FromFacing, ToFacing, progress, Distance);
return false;
}
protected abstract MovePart OnComplete(Actor self, Mobile mobile, Move parent);
public override IEnumerable<Target> GetTargets(Actor self)
{
return Move.GetTargets(self);
}
}
class MoveFirstHalf : MovePart
{
public MoveFirstHalf(Move move, WPos from, WPos to, WAngle fromFacing, WAngle toFacing, int carryoverProgress)
: base(move, from, to, fromFacing, toFacing, carryoverProgress) { }
static bool IsTurn(Mobile mobile, CPos nextCell, Map map)
{
// Some actors with a limited number of sprite facings should never move along curved trajectories.
if (mobile.Info.AlwaysTurnInPlace)
return false;
// Tight U-turns should be done in place instead of making silly looking loops.
var nextFacing = map.FacingBetween(nextCell, mobile.ToCell, mobile.Facing);
var currentFacing = map.FacingBetween(mobile.ToCell, mobile.FromCell, mobile.Facing);
var delta = (nextFacing - currentFacing).Angle;
return delta != 0 && (delta < 384 || delta > 640);
}
protected override MovePart OnComplete(Actor self, Mobile mobile, Move parent)
{
var map = self.World.Map;
var fromSubcellOffset = map.Grid.OffsetOfSubCell(mobile.FromSubCell);
var toSubcellOffset = map.Grid.OffsetOfSubCell(mobile.ToSubCell);
var nextCell = parent.PopPath(self);
if (nextCell != null)
{
if (!mobile.IsTraitPaused && !mobile.IsTraitDisabled && IsTurn(mobile, nextCell.Value.Cell, map))
{
var nextSubcellOffset = map.Grid.OffsetOfSubCell(nextCell.Value.SubCell);
var ret = new MoveFirstHalf(
Move,
Util.BetweenCells(self.World, mobile.FromCell, mobile.ToCell) + (fromSubcellOffset + toSubcellOffset) / 2,
Util.BetweenCells(self.World, mobile.ToCell, nextCell.Value.Cell) + (toSubcellOffset + nextSubcellOffset) / 2,
mobile.Facing,
map.FacingBetween(mobile.ToCell, nextCell.Value.Cell, mobile.Facing),
progress - Distance);
mobile.FinishedMoving(self);
mobile.SetLocation(mobile.ToCell, mobile.ToSubCell, nextCell.Value.Cell, nextCell.Value.SubCell);
return ret;
}
parent.path.Add(nextCell.Value.Cell);
}
var toPos = mobile.ToCell.Layer == 0 ? map.CenterOfCell(mobile.ToCell) :
self.World.GetCustomMovementLayers()[mobile.ToCell.Layer].CenterOfCell(mobile.ToCell);
var ret2 = new MoveSecondHalf(
Move,
Util.BetweenCells(self.World, mobile.FromCell, mobile.ToCell) + (fromSubcellOffset + toSubcellOffset) / 2,
toPos + toSubcellOffset,
mobile.Facing,
mobile.Facing,
progress - Distance);
mobile.EnteringCell(self);
mobile.SetLocation(mobile.ToCell, mobile.ToSubCell, mobile.ToCell, mobile.ToSubCell);
return ret2;
}
}
class MoveSecondHalf : MovePart
{
public MoveSecondHalf(Move move, WPos from, WPos to, WAngle fromFacing, WAngle toFacing, int carryoverProgress)
: base(move, from, to, fromFacing, toFacing, carryoverProgress) { }
protected override MovePart OnComplete(Actor self, Mobile mobile, Move parent)
{
mobile.SetPosition(self, mobile.ToCell);
// Move might immediately queue a new MoveFirstHalf within the same tick if we haven't
// reached the end of the requested path. Make sure that any leftover movement progress is
// correctly carried over into this new activity to avoid a glitch in the apparent move speed.
Move.carryoverProgress = progress - Distance;
return null;
}
}
}
}