move Mobile et al into Mods/

This commit is contained in:
Bob
2010-10-22 11:31:13 +13:00
parent 6513bd5fe0
commit 0d9cf63dd2
34 changed files with 68 additions and 42 deletions

61
OpenRA.Mods.RA/Move/Drag.cs Executable file
View File

@@ -0,0 +1,61 @@
#region Copyright & License Information
/*
* Copyright 2007-2010 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. For more information,
* see LICENSE.
*/
#endregion
using System.Collections.Generic;
using OpenRA.Traits;
namespace OpenRA.Mods.RA.Move
{
public class Drag : IActivity
{
IActivity NextActivity { get; set; }
int2 endLocation;
int2 startLocation;
int length;
public Drag(int2 start, int2 end, int length)
{
startLocation = start;
endLocation = end;
this.length = length;
}
int ticks = 0;
public IActivity Tick( Actor self )
{
var mobile = self.Trait<Mobile>();
mobile.PxPosition = int2.Lerp(startLocation, endLocation, ticks, length - 1);
if (++ticks >= length)
{
mobile.IsMoving = false;
return NextActivity;
}
mobile.IsMoving = true;
return this;
}
public void Cancel(Actor self) { }
public void Queue( IActivity activity )
{
if( NextActivity != null )
NextActivity.Queue( activity );
else
NextActivity = activity;
}
public IEnumerable<float2> GetCurrentPath()
{
yield return endLocation;
}
}
}

403
OpenRA.Mods.RA/Move/Mobile.cs Executable file
View File

@@ -0,0 +1,403 @@
#region Copyright & License Information
/*
* Copyright 2007-2010 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. For more information,
* see LICENSE.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.Effects;
using OpenRA.Traits.Activities;
using OpenRA.FileFormats;
using System.Diagnostics;
using OpenRA.Orders;
using OpenRA.Traits;
namespace OpenRA.Mods.RA.Move
{
public class MobileInfo : ITraitInfo
{
[FieldLoader.LoadUsing( "LoadSpeeds" )]
public readonly Dictionary<string, TerrainInfo> TerrainSpeeds;
[FieldLoader.Load] public readonly string[] Crushes;
[FieldLoader.Load] public readonly int WaitAverage = 60;
[FieldLoader.Load] public readonly int WaitSpread = 20;
[FieldLoader.Load] public readonly int InitialFacing = 128;
[FieldLoader.Load] public readonly int ROT = 255;
[FieldLoader.Load] public readonly int Speed = 1;
[FieldLoader.Load] public readonly bool OnRails = false;
public virtual object Create(ActorInitializer init) { return new Mobile(init, this); }
static object LoadSpeeds( MiniYaml y )
{
Dictionary<string,TerrainInfo> ret = new Dictionary<string, TerrainInfo>();
foreach (var t in y.NodesDict["TerrainSpeeds"].Nodes)
{
var speed = (float)FieldLoader.GetValue("speed", typeof(float),t.Value.Value);
var cost = t.Value.NodesDict.ContainsKey("PathingCost") ? (int)FieldLoader.GetValue("cost", typeof(int), t.Value.NodesDict["PathingCost"].Value) : (int)(10000/speed);
ret.Add(t.Key, new TerrainInfo{Speed = speed, Cost = cost});
}
return ret;
}
public class TerrainInfo
{
public int Cost = int.MaxValue;
public float Speed = 0;
}
}
public class Mobile : IIssueOrder, IResolveOrder, IOrderVoice, IOccupySpace, IMove, IFacing, INudge
{
public readonly Actor self;
public readonly MobileInfo Info;
public bool IsMoving { get; internal set; }
int __facing;
int2 __fromCell, __toCell;
int __altitude;
[Sync]
public int Facing
{
get { return __facing; }
set { __facing = value; }
}
[Sync]
public int Altitude
{
get { return __altitude; }
set { __altitude = value; }
}
public int ROT { get { return Info.ROT; } }
public int InitialFacing { get { return Info.InitialFacing; } }
[Sync]
public int2 PxPosition { get; set; }
[Sync]
public int2 fromCell { get { return __fromCell; } }
[Sync]
public int2 toCell { get { return __toCell; } }
[Sync]
public int PathHash; // written by Move.EvalPath, to temporarily debug this crap.
public void SetLocation(int2 from, int2 to)
{
if (fromCell == from && toCell == to) return;
RemoveInfluence();
__fromCell = from;
__toCell = to;
AddInfluence();
}
UnitInfluence uim;
const int avgTicksBeforePathing = 5;
const int spreadTicksBeforePathing = 5;
internal int ticksBeforePathing = 0;
public Mobile(ActorInitializer init, MobileInfo info)
{
this.self = init.self;
this.Info = info;
uim = self.World.WorldActor.Trait<UnitInfluence>();
if (init.Contains<LocationInit>())
{
this.__fromCell = this.__toCell = init.Get<LocationInit,int2>();
this.PxPosition = Util.CenterOfCell( fromCell );
AddInfluence();
}
this.Facing = init.Contains<FacingInit>() ? init.Get<FacingInit,int>() : info.InitialFacing;
this.Altitude = init.Contains<AltitudeInit>() ? init.Get<AltitudeInit,int>() : 0;
}
public void SetPosition(Actor self, int2 cell)
{
SetLocation( cell, cell );
PxPosition = Util.CenterOfCell(fromCell);
}
public void SetPxPosition( Actor self, int2 px )
{
var cell = Util.CellContaining( px );
SetLocation( cell, cell );
PxPosition = px;
}
public IEnumerable<IOrderTargeter> Orders { get { yield return new MoveOrderTargeter( Info ); } }
// Note: Returns a valid order even if the unit can't move to the target
public Order IssueOrder( Actor self, IOrderTargeter order, Target target )
{
if( order is MoveOrderTargeter )
{
if( Info.OnRails ) return null;
return new Order( "Move", self, Util.CellContaining( target.CenterLocation ), false );
}
return null;
}
public int2 NearestMoveableCell(int2 target)
{
if (CanEnterCell(target))
return target;
var searched = new List<int2>(){};
// Limit search to a radius of 10 tiles
for (int r = 1; r < 10; r++)
foreach (var tile in self.World.FindTilesInCircle(target,r).Except(searched))
{
if (CanEnterCell(tile))
return tile;
searched.Add(tile);
}
// Couldn't find a cell
return target;
}
public void ResolveOrder(Actor self, Order order)
{
if (order.OrderString == "Move")
{
int2 currentLocation = NearestMoveableCell(order.TargetLocation);
if (!CanEnterCell(currentLocation))
return;
if( !order.Queued ) self.CancelActivity();
self.QueueActivity(new Move(currentLocation, 8));
if (self.Owner == self.World.LocalPlayer)
self.World.AddFrameEndTask(w =>
{
if (self.Destroyed) return;
w.Add(new MoveFlash(self.World, order.TargetLocation));
var line = self.TraitOrDefault<DrawLineToTarget>();
if (line != null)
line.SetTarget(self, Target.FromCell(currentLocation), Color.Green);
});
ticksBeforePathing = avgTicksBeforePathing +
self.World.SharedRandom.Next( -spreadTicksBeforePathing, spreadTicksBeforePathing );
}
}
public string VoicePhraseForOrder(Actor self, Order order)
{
if (order.OrderString == "Move")
return "Move";
return null;
}
public int2 TopLeft { get { return toCell; } }
public IEnumerable<int2> OccupiedCells()
{
return (fromCell == toCell)
? new[] { fromCell }
: CanEnterCell(toCell)
? new[] { toCell }
: new[] { fromCell, toCell };
}
public bool CanEnterCell(int2 p)
{
return CanEnterCell( p, null, true);
}
public static bool CanEnterCell( World world, MobileInfo mi, int2 cell, Actor ignoreActor, bool checkTransientActors )
{
var bim = world.WorldActor.Trait<BuildingInfluence>();
var uim = world.WorldActor.Trait<UnitInfluence>();
return Mobile.CanEnterCell( mi, world, uim, bim, cell, ignoreActor, checkTransientActors );
}
public bool CanEnterCell( int2 cell, Actor ignoreActor, bool checkTransientActors )
{
var bim = self.World.WorldActor.Trait<BuildingInfluence>();
var uim = self.World.WorldActor.Trait<UnitInfluence>();
return CanEnterCell( Info, self.World, uim, bim, cell, ignoreActor, checkTransientActors );
}
public static bool CanEnterCell( MobileInfo mobileInfo, World world, UnitInfluence uim, BuildingInfluence bim, int2 cell, Actor ignoreActor, bool checkTransientActors )
{
if (MovementCostForCell(mobileInfo, world, cell) == int.MaxValue)
return false;
// Check for buildings
var building = bim.GetBuildingBlocking(cell);
if (building != null && building != ignoreActor)
{
if (mobileInfo.Crushes == null)
return false;
var crushable = building.TraitsImplementing<ICrushable>();
if (crushable.Count() == 0)
return false;
if (!crushable.Any(b => b.CrushClasses.Intersect(mobileInfo.Crushes).Any()))
return false;
}
// Check mobile actors
var blockingActors = uim.GetUnitsAt( cell ).Where( x => x != ignoreActor ).ToList();
if (checkTransientActors && blockingActors.Count > 0)
{
// We can enter a cell with nonshareable units only if we can crush all of them
if (mobileInfo.Crushes == null)
return false;
if (blockingActors.Any(a => !(a.HasTrait<ICrushable>() &&
a.TraitsImplementing<ICrushable>().Any(b => b.CrushClasses.Intersect(mobileInfo.Crushes).Any()))))
return false;
}
return true;
}
public void FinishedMoving(Actor self)
{
var crushable = uim.GetUnitsAt(toCell).Where(a => a != self && a.HasTrait<ICrushable>());
foreach (var a in crushable)
{
var crushActions = a.TraitsImplementing<ICrushable>().Where(b => b.CrushClasses.Intersect(Info.Crushes).Any());
foreach (var b in crushActions)
b.OnCrush(self);
}
}
public static int MovementCostForCell(MobileInfo info, World world, int2 cell)
{
if (!world.Map.IsInMap(cell.X,cell.Y))
return int.MaxValue;
var type = world.GetTerrainType(cell);
if (!info.TerrainSpeeds.ContainsKey(type))
return int.MaxValue;
return info.TerrainSpeeds[type].Cost;
}
public float MovementSpeedForCell(Actor self, int2 cell)
{
var type = self.World.GetTerrainType(cell);
if (!Info.TerrainSpeeds.ContainsKey(type))
return 0;
var modifier = self
.TraitsImplementing<ISpeedModifier>()
.Select(t => t.GetSpeedModifier())
.Product();
return Info.Speed * Info.TerrainSpeeds[type].Speed * modifier / 100f;
}
public void AddInfluence()
{
uim.Add( self, this );
}
public void RemoveInfluence()
{
uim.Remove( self, this );
}
public void OnNudge(Actor self, Actor nudger)
{
/* initial fairly braindead implementation. */
if (self.Owner.Stances[nudger.Owner] != Stance.Ally)
return; /* don't allow ourselves to be pushed around
* by the enemy! */
if (!self.IsIdle)
return; /* don't nudge if we're busy doing something! */
// pick an adjacent available cell.
var availCells = new List<int2>();
var notStupidCells = new List<int2>();
for( var i = -1; i < 2; i++ )
for (var j = -1; j < 2; j++)
{
var p = toCell + new int2(i, j);
if (CanEnterCell(p))
availCells.Add(p);
else
if (p != nudger.Location && p != toCell)
notStupidCells.Add(p);
}
var moveTo = availCells.Any() ? availCells.Random(self.World.SharedRandom) :
notStupidCells.Any() ? notStupidCells.Random(self.World.SharedRandom) : (int2?)null;
if (moveTo.HasValue)
{
self.CancelActivity();
if (self.Owner == self.World.LocalPlayer)
self.World.AddFrameEndTask(w =>
{
if (self.Destroyed) return;
var line = self.TraitOrDefault<DrawLineToTarget>();
if (line != null)
line.SetTargetSilently(self, Target.FromCell(moveTo.Value), Color.Green);
});
self.QueueActivity(new Move(moveTo.Value, 0));
Log.Write("debug", "OnNudge #{0} from {1} to {2}",
self.ActorID, self.Location, moveTo.Value);
}
else
Log.Write("debug", "OnNudge #{0} refuses at {1}",
self.ActorID, self.Location);
}
class MoveOrderTargeter : IOrderTargeter
{
readonly MobileInfo unitType;
public MoveOrderTargeter( MobileInfo unitType )
{
this.unitType = unitType;
}
public string OrderID { get { return "Move"; } }
public int OrderPriority { get { return 4; } }
public bool CanTargetUnit( Actor self, Actor target, bool forceAttack, bool forceMove, ref string cursor )
{
return false;
}
public bool CanTargetLocation( Actor self, int2 location, List<Actor> actorsAtLocation, bool forceAttack, bool forceMove, ref string cursor )
{
cursor = "move";
if( self.World.LocalPlayer.Shroud.IsVisible( location ) && !self.Trait<Mobile>().CanEnterCell( location ) )
cursor = "move-blocked";
return true;
}
}
public IActivity MoveTo( int2 cell ) { return new Move( cell ); }
public IActivity MoveTo( int2 cell, int range ) { return new Move( cell, range ); }
public IActivity MoveTo( int2 cell, Actor ignoredActor ) { return new Move( cell, ignoredActor ); }
public IActivity MoveTo( Actor target, int range ) { return new Move( target, range ); }
public IActivity MoveTo( Target target, int range ) { return new Move( target, range ); }
public IActivity MoveTo( Func<List<int2>> pathFunc ) { return new Move( pathFunc ); }
}
}

376
OpenRA.Mods.RA/Move/Move.cs Executable file
View File

@@ -0,0 +1,376 @@
#region Copyright & License Information
/*
* Copyright 2007-2010 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. For more information,
* see LICENSE.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenRA.Traits;
using OpenRA.Traits.Activities;
namespace OpenRA.Mods.RA.Move
{
class Move : CancelableActivity
{
int2? destination;
int nearEnough;
public List<int2> path;
Func<Actor, Mobile, List<int2>> getPath;
public Actor ignoreBuilding;
// Scriptable move order
// Ignores lane bias and nearby units
public Move( int2 destination )
{
this.getPath = (self,mobile) =>
self.World.WorldActor.Trait<PathFinder>().FindPath(
PathSearch.FromPoint( self.World, mobile.Info, mobile.toCell, destination, false )
.WithoutLaneBias());
this.destination = destination;
this.nearEnough = 0;
}
public Move( int2 destination, int nearEnough )
{
this.getPath = (self,mobile) => self.World.WorldActor.Trait<PathFinder>().FindUnitPath( mobile.toCell, destination, self );
this.destination = destination;
this.nearEnough = nearEnough;
}
public Move(int2 destination, Actor ignoreBuilding)
{
this.getPath = (self,mobile) =>
self.World.WorldActor.Trait<PathFinder>().FindPath(
PathSearch.FromPoint( self.World, mobile.Info, mobile.toCell, destination, false )
.WithCustomBlocker( self.World.WorldActor.Trait<PathFinder>().AvoidUnitsNear( mobile.toCell, 4, self ))
.WithIgnoredBuilding( ignoreBuilding ));
this.destination = destination;
this.nearEnough = 0;
this.ignoreBuilding = ignoreBuilding;
}
public Move( Actor target, int range )
{
this.getPath = (self,mobile) => self.World.WorldActor.Trait<PathFinder>().FindUnitPathToRange(
mobile.toCell, target.Location,
range, self );
this.destination = null;
this.nearEnough = range;
}
public Move(Target target, int range)
{
this.getPath = (self,mobile) => self.World.WorldActor.Trait<PathFinder>().FindUnitPathToRange(
mobile.toCell, Util.CellContaining(target.CenterLocation),
range, self);
this.destination = null;
this.nearEnough = range;
}
public Move(Func<List<int2>> getPath)
{
this.getPath = (_1,_2) => getPath();
this.destination = null;
this.nearEnough = 0;
}
static int HashList<T>(List<T> xs)
{
int hash = 0;
int n = 0;
foreach (var x in xs)
hash += n++ * x.GetHashCode();
return hash;
}
List<int2> EvalPath( Actor self, Mobile mobile )
{
var path = getPath(self, mobile).TakeWhile(a => a != mobile.toCell).ToList();
mobile.PathHash = HashList(path);
Log.Write("debug", "EvalPathHash #{0} {1}",
self.ActorID, mobile.PathHash);
return path;
}
public override IActivity Tick( Actor self )
{
var mobile = self.Trait<Mobile>();
if (destination == mobile.toCell)
return NextActivity;
if( path == null )
{
if (mobile.ticksBeforePathing > 0)
{
--mobile.ticksBeforePathing;
return this;
}
path = EvalPath(self, mobile);
SanityCheckPath( mobile );
}
if( path.Count == 0 )
{
destination = mobile.toCell;
return this;
}
destination = path[ 0 ];
var nextCell = PopPath( self, mobile );
if( nextCell == null )
return this;
int2 dir = nextCell.Value - mobile.fromCell;
var firstFacing = Util.GetFacing( dir, mobile.Facing );
if( firstFacing != mobile.Facing )
{
path.Add( nextCell.Value );
return Util.SequenceActivities( new Turn( firstFacing ), this ).Tick( self );
}
else
{
mobile.SetLocation( mobile.fromCell, nextCell.Value );
var move = new MoveFirstHalf(
this,
Util.CenterOfCell( mobile.fromCell ),
Util.BetweenCells( mobile.fromCell, mobile.toCell ),
mobile.Facing,
mobile.Facing,
0 );
return move.Tick( self );
}
}
[Conditional( "SANITY_CHECKS")]
void SanityCheckPath( Mobile mobile )
{
if( path.Count == 0 )
return;
var d = path[path.Count-1] - mobile.toCell;
if( d.LengthSquared > 2 )
throw new InvalidOperationException( "(Move) Sanity check failed" );
}
bool hasWaited;
bool hasNudged;
int waitTicksRemaining;
void NudgeBlocker(Actor self, int2 nextCell)
{
var blocker = self.World.WorldActor.Trait<UnitInfluence>().GetUnitsAt(nextCell).FirstOrDefault();
if (blocker == null) return;
Log.Write("debug", "NudgeBlocker #{0} nudges #{1} at {2} from {3}",
self.ActorID, blocker.ActorID, nextCell, self.Location);
var nudge = blocker.TraitOrDefault<INudge>();
if (nudge != null)
nudge.OnNudge(blocker, self);
}
int2? PopPath( Actor self, Mobile mobile )
{
if( path.Count == 0 ) return null;
var nextCell = path[ path.Count - 1 ];
if( !mobile.CanEnterCell( nextCell, ignoreBuilding, true ) )
{
if( ( mobile.toCell - destination.Value ).LengthSquared <= nearEnough )
{
path.Clear();
return null;
}
if (!hasNudged)
{
NudgeBlocker(self, nextCell);
hasNudged = true;
}
if (!hasWaited)
{
var info = self.Info.Traits.Get<MobileInfo>();
waitTicksRemaining = info.WaitAverage + self.World.SharedRandom.Next(-info.WaitSpread, info.WaitSpread);
hasWaited = true;
}
if (--waitTicksRemaining >= 0)
return null;
if (mobile.ticksBeforePathing > 0)
{
--mobile.ticksBeforePathing;
return null;
}
mobile.RemoveInfluence();
var newPath = EvalPath(self, mobile);
mobile.AddInfluence();
if (newPath.Count != 0)
path = newPath;
return null;
}
hasNudged = false;
hasWaited = false;
path.RemoveAt( path.Count - 1 );
return nextCell;
}
protected override bool OnCancel()
{
path = new List<int2>();
return true;
}
public override IEnumerable<float2> GetCurrentPath()
{
if( path != null )
return Enumerable.Reverse(path).Select( c => (float2)Util.CenterOfCell(c) );
if( destination != null )
return new float2[] { destination.Value };
return new float2[ 0 ];
}
abstract class MovePart : IActivity
{
public readonly Move move;
public readonly int2 from, to;
public readonly int fromFacing, toFacing;
public int moveFraction;
public readonly int moveFractionTotal;
public MovePart( Move move, int2 from, int2 to, int fromFacing, int toFacing, int startingFraction )
{
this.move = move;
this.from = from;
this.to = to;
this.fromFacing = fromFacing;
this.toFacing = toFacing;
this.moveFraction = startingFraction;
this.moveFractionTotal = ( ( to - from ) * 3 ).Length;
}
public void Cancel( Actor self )
{
move.Cancel( self );
}
public void Queue( IActivity activity )
{
move.Queue( activity );
}
public IActivity Tick( Actor self )
{
var mobile = self.Trait<Mobile>();
var ret = InnerTick( self, mobile );
mobile.IsMoving = ( ret is MovePart );
if( moveFraction > moveFractionTotal )
moveFraction = moveFractionTotal;
UpdateCenterLocation( self, mobile );
return ret;
}
IActivity InnerTick( Actor self, Mobile mobile )
{
moveFraction += (int)mobile.MovementSpeedForCell(self, mobile.toCell);
if( moveFraction <= moveFractionTotal )
return this;
var next = OnComplete( self, mobile, move );
if( next != null )
return next;
return move;
}
void UpdateCenterLocation( Actor self, Mobile mobile )
{
mobile.PxPosition = int2.Lerp( from, to, moveFraction, moveFractionTotal );
if( moveFraction >= moveFractionTotal )
mobile.Facing = toFacing & 0xFF;
else
mobile.Facing = int2.Lerp( fromFacing, toFacing, moveFraction, moveFractionTotal ) & 0xFF;
}
protected abstract MovePart OnComplete( Actor self, Mobile mobile, Move parent );
public IEnumerable<float2> GetCurrentPath()
{
return move.GetCurrentPath();
}
}
class MoveFirstHalf : MovePart
{
public MoveFirstHalf( Move move, int2 from, int2 to, int fromFacing, int toFacing, int startingFraction )
: base( move, from, to, fromFacing, toFacing, startingFraction )
{
}
protected override MovePart OnComplete( Actor self, Mobile mobile, Move parent )
{
var nextCell = parent.PopPath( self, mobile );
if( nextCell != null )
{
if( ( nextCell - mobile.toCell ) != ( mobile.toCell - mobile.fromCell ) )
{
var ret = new MoveFirstHalf(
move,
Util.BetweenCells( mobile.fromCell, mobile.toCell ),
Util.BetweenCells( mobile.toCell, nextCell.Value ),
mobile.Facing,
Util.GetNearestFacing( mobile.Facing, Util.GetFacing( nextCell.Value - mobile.toCell, mobile.Facing ) ),
moveFraction - moveFractionTotal );
mobile.SetLocation( mobile.toCell, nextCell.Value );
return ret;
}
else
parent.path.Add( nextCell.Value );
}
var ret2 = new MoveSecondHalf(
move,
Util.BetweenCells( mobile.fromCell, mobile.toCell ),
Util.CenterOfCell( mobile.toCell ),
mobile.Facing,
mobile.Facing,
moveFraction - moveFractionTotal );
mobile.SetLocation( mobile.toCell, mobile.toCell );
return ret2;
}
}
class MoveSecondHalf : MovePart
{
public MoveSecondHalf( Move move, int2 from, int2 to, int fromFacing, int toFacing, int startingFraction )
: base( move, from, to, fromFacing, toFacing, startingFraction )
{
}
protected override MovePart OnComplete( Actor self, Mobile mobile, Move parent )
{
mobile.PxPosition = Util.CenterOfCell( mobile.toCell );
mobile.SetLocation( mobile.toCell, mobile.toCell );
mobile.FinishedMoving(self);
return null;
}
}
}
}

240
OpenRA.Mods.RA/Move/PathFinder.cs Executable file
View File

@@ -0,0 +1,240 @@
#region Copyright & License Information
/*
* Copyright 2007-2010 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. For more information,
* see LICENSE.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Mods.RA.Move
{
public class PathFinderInfo : ITraitInfo
{
public object Create( ActorInitializer init ) { return new PathFinder( init.world ); }
}
public class PathFinder
{
readonly World world;
public PathFinder( World world ) { this.world = world; }
class CachedPath
{
public int2 from;
public int2 to;
public List<int2> result;
public int tick;
public Actor actor;
}
List<CachedPath> CachedPaths = new List<CachedPath>();
const int MaxPathAge = 50; /* x 40ms ticks */
public List<int2> FindUnitPath(int2 from, int2 target, Actor self)
{
using (new PerfSample("Pathfinder"))
{
var cached = CachedPaths.FirstOrDefault(p => p.from == from && p.to == target && p.actor == self);
if (cached != null)
{
Log.Write("debug", "Actor {0} asked for a path from {1} tick(s) ago", self.ActorID, world.FrameNumber - cached.tick);
cached.tick = world.FrameNumber;
return new List<int2>(cached.result);
}
var mi = self.Info.Traits.Get<MobileInfo>();
var pb = FindBidiPath(
PathSearch.FromPoint(world, mi, target, from, true)
.WithCustomBlocker(AvoidUnitsNear(from, 4, self)),
PathSearch.FromPoint(world, mi, from, target, true)
.WithCustomBlocker(AvoidUnitsNear(from, 4, self))
.InReverse());
CheckSanePath2(pb, from, target);
CachedPaths.RemoveAll(p => world.FrameNumber - p.tick > MaxPathAge);
CachedPaths.Add(new CachedPath { from = from, to = target, actor = self, result = pb, tick = world.FrameNumber });
return new List<int2>(pb);
}
}
public List<int2> FindUnitPathToRange( int2 src, int2 target, int range, Actor self )
{
using( new PerfSample( "Pathfinder" ) )
{
var mobileInfo = self.Info.Traits.Get<MobileInfo>();
var tilesInRange = world.FindTilesInCircle(target, range)
.Where( t => Mobile.CanEnterCell(self.World, mobileInfo, t, null, true));
var path = FindPath( PathSearch.FromPoints( world, mobileInfo, tilesInRange, src, false )
.WithCustomBlocker(AvoidUnitsNear(src, 4, self))
.InReverse());
path.Reverse();
return path;
}
}
public Func<int2, bool> AvoidUnitsNear(int2 p, int dist, Actor self)
{
return q =>
p != q &&
((p - q).LengthSquared < dist * dist) &&
(world.WorldActor.Trait<UnitInfluence>().GetUnitsAt(q).Any(a => a.Group != self.Group));
}
public List<int2> FindPath( PathSearch search )
{
using (new PerfSample("Pathfinder"))
{
while (!search.queue.Empty)
{
var p = search.Expand( world );
if (search.heuristic(p) == 0)
return MakePath(search.cellInfo, p);
}
// no path exists
return new List<int2>();
}
}
static List<int2> MakePath( CellInfo[ , ] cellInfo, int2 destination )
{
List<int2> ret = new List<int2>();
int2 pathNode = destination;
while( cellInfo[ pathNode.X, pathNode.Y ].Path != pathNode )
{
ret.Add( pathNode );
pathNode = cellInfo[ pathNode.X, pathNode.Y ].Path;
}
ret.Add(pathNode);
CheckSanePath(ret);
return ret;
}
public List<int2> FindBidiPath( /* searches from both ends toward each other */
PathSearch fromSrc,
PathSearch fromDest)
{
using (new PerfSample("Pathfinder"))
{
while (!fromSrc.queue.Empty && !fromDest.queue.Empty)
{
/* make some progress on the first search */
var p = fromSrc.Expand( world );
if (fromDest.cellInfo[p.X, p.Y].Seen && fromDest.cellInfo[p.X, p.Y].MinCost < float.PositiveInfinity)
return MakeBidiPath(fromSrc, fromDest, p);
/* make some progress on the second search */
var q = fromDest.Expand( world );
if (fromSrc.cellInfo[q.X, q.Y].Seen && fromSrc.cellInfo[q.X, q.Y].MinCost < float.PositiveInfinity)
return MakeBidiPath(fromSrc, fromDest, q);
}
return new List<int2>();
}
}
static List<int2> MakeBidiPath(PathSearch a, PathSearch b, int2 p)
{
var ca = a.cellInfo;
var cb = b.cellInfo;
var ret = new List<int2>();
var q = p;
while (ca[q.X, q.Y].Path != q)
{
ret.Add( q );
q = ca[ q.X, q.Y ].Path;
}
ret.Add(q);
ret.Reverse();
q = p;
while (cb[q.X, q.Y].Path != q)
{
q = cb[q.X, q.Y].Path;
ret.Add(q);
}
CheckSanePath( ret );
return ret;
}
[Conditional( "SANITY_CHECKS" )]
static void CheckSanePath( List<int2> path )
{
if( path.Count == 0 )
return;
var prev = path[ 0 ];
for( int i = 0 ; i < path.Count ; i++ )
{
var d = path[ i ] - prev;
if( Math.Abs( d.X ) > 1 || Math.Abs( d.Y ) > 1 )
throw new InvalidOperationException( "(PathFinder) path sanity check failed" );
prev = path[ i ];
}
}
[Conditional("SANITY_CHECKS")]
static void CheckSanePath2(List<int2> path, int2 src, int2 dest)
{
if (path.Count == 0)
return;
if (path[0] != dest)
throw new InvalidOperationException("(PathFinder) sanity check failed: doesn't go to dest");
if (path[path.Count - 1] != src)
throw new InvalidOperationException("(PathFinder) sanity check failed: doesn't come from src");
}
}
public struct CellInfo
{
public int MinCost;
public int2 Path;
public bool Seen;
public CellInfo( int minCost, int2 path, bool seen )
{
MinCost = minCost;
Path = path;
Seen = seen;
}
}
public struct PathDistance : IComparable<PathDistance>
{
public int EstTotal;
public int2 Location;
public PathDistance(int estTotal, int2 location)
{
EstTotal = estTotal;
Location = location;
}
public int CompareTo(PathDistance other)
{
return Math.Sign(EstTotal - other.EstTotal);
}
}
}

218
OpenRA.Mods.RA/Move/PathSearch.cs Executable file
View File

@@ -0,0 +1,218 @@
#region Copyright & License Information
/*
* Copyright 2007-2010 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. For more information,
* see LICENSE.
*/
#endregion
using System;
using System.Collections.Generic;
using OpenRA.FileFormats;
using OpenRA.Traits;
namespace OpenRA.Mods.RA.Move
{
public class PathSearch
{
World world;
public CellInfo[ , ] cellInfo;
public PriorityQueue<PathDistance> queue;
public Func<int2, int> heuristic;
Func<int2, bool> customBlock;
public bool checkForBlocked;
public Actor ignoreBuilding;
public bool inReverse;
MobileInfo mobileInfo;
BuildingInfluence bim;
UnitInfluence uim;
public PathSearch(World world, MobileInfo mobileInfo)
{
this.world = world;
bim = world.WorldActor.Trait<BuildingInfluence>();
uim = world.WorldActor.Trait<UnitInfluence>();
cellInfo = InitCellInfo();
this.mobileInfo = mobileInfo;
queue = new PriorityQueue<PathDistance>();
}
public PathSearch InReverse()
{
inReverse = true;
return this;
}
public PathSearch WithCustomBlocker(Func<int2, bool> customBlock)
{
this.customBlock = customBlock;
return this;
}
public PathSearch WithIgnoredBuilding(Actor b)
{
ignoreBuilding = b;
return this;
}
public PathSearch WithHeuristic(Func<int2, int> h)
{
heuristic = h;
return this;
}
public PathSearch WithoutLaneBias()
{
LaneBias = 0;
return this;
}
public PathSearch FromPoint(int2 from)
{
AddInitialCell( from );
return this;
}
int LaneBias = 1;
public int2 Expand( World world )
{
var p = queue.Pop();
while (cellInfo[p.Location.X, p.Location.Y].Seen)
if (queue.Empty)
return p.Location;
else
p = queue.Pop();
cellInfo[p.Location.X, p.Location.Y].Seen = true;
var thisCost = Mobile.MovementCostForCell(mobileInfo, world, p.Location);
if (thisCost == int.MaxValue)
return p.Location;
foreach( int2 d in directions )
{
int2 newHere = p.Location + d;
if (!world.Map.IsInMap(newHere.X, newHere.Y)) continue;
if( cellInfo[ newHere.X, newHere.Y ].Seen )
continue;
var costHere = Mobile.MovementCostForCell(mobileInfo, world, newHere);
if (costHere == int.MaxValue)
continue;
if (!Mobile.CanEnterCell(mobileInfo, world, uim, bim, newHere, ignoreBuilding, checkForBlocked))
continue;
if (customBlock != null && customBlock(newHere))
continue;
var est = heuristic( newHere );
if( est == int.MaxValue )
continue;
int cellCost = costHere;
if( d.X * d.Y != 0 ) cellCost = ( cellCost * 34 ) / 24;
// directional bonuses for smoother flow!
var ux = (newHere.X + (inReverse ? 1 : 0) & 1);
var uy = (newHere.Y + (inReverse ? 1 : 0) & 1);
if (ux == 0 && d.Y < 0) cellCost += LaneBias;
else if (ux == 1 && d.Y > 0) cellCost += LaneBias;
if (uy == 0 && d.X < 0) cellCost += LaneBias;
else if (uy == 1 && d.X > 0) cellCost += LaneBias;
int newCost = cellInfo[ p.Location.X, p.Location.Y ].MinCost + cellCost;
if( newCost >= cellInfo[ newHere.X, newHere.Y ].MinCost )
continue;
cellInfo[ newHere.X, newHere.Y ].Path = p.Location;
cellInfo[ newHere.X, newHere.Y ].MinCost = newCost;
queue.Add( new PathDistance( newCost + est, newHere ) );
}
return p.Location;
}
static readonly int2[] directions =
{
new int2( -1, -1 ),
new int2( -1, 0 ),
new int2( -1, 1 ),
new int2( 0, -1 ),
new int2( 0, 1 ),
new int2( 1, -1 ),
new int2( 1, 0 ),
new int2( 1, 1 ),
};
public void AddInitialCell( int2 location )
{
if (!world.Map.IsInMap(location.X, location.Y))
return;
cellInfo[ location.X, location.Y ] = new CellInfo( 0, location, false );
queue.Add( new PathDistance( heuristic( location ), location ) );
}
public static PathSearch Search( World world, MobileInfo mi, bool checkForBlocked )
{
var search = new PathSearch(world, mi) {
checkForBlocked = checkForBlocked };
return search;
}
public static PathSearch FromPoint( World world, MobileInfo mi, int2 from, int2 target, bool checkForBlocked )
{
var search = new PathSearch(world, mi) {
heuristic = DefaultEstimator( target ),
checkForBlocked = checkForBlocked };
search.AddInitialCell( from );
return search;
}
public static PathSearch FromPoints(World world, MobileInfo mi, IEnumerable<int2> froms, int2 target, bool checkForBlocked)
{
var search = new PathSearch(world, mi)
{
heuristic = DefaultEstimator(target),
checkForBlocked = checkForBlocked
};
foreach( var sl in froms )
search.AddInitialCell( sl );
return search;
}
CellInfo[ , ] InitCellInfo()
{
var cellInfo = new CellInfo[ world.Map.MapSize.X, world.Map.MapSize.Y ];
for( int x = 0 ; x < world.Map.MapSize.X ; x++ )
for( int y = 0 ; y < world.Map.MapSize.Y ; y++ )
cellInfo[ x, y ] = new CellInfo( int.MaxValue, new int2( x, y ), false );
return cellInfo;
}
public static Func<int2, int> DefaultEstimator( int2 destination )
{
return here =>
{
int2 d = ( here - destination ).Abs();
int diag = Math.Min( d.X, d.Y );
int straight = Math.Abs( d.X - d.Y );
return (3400 * diag / 24) + (100 * straight);
};
}
}
}