For one straightforward game, I needed to implement the AI behavior with basic functionality: patrolling, pursuit, and combat. The task itself is simple, however, there were two types of locations and with different levels of abstraction.
In one case, the action took place in confined spaces, and in the other, in the middle of city streets. In small spaces, a navigation grid was generated, but in a large location, graph pathfinding was used to maintain performance.
All kinds of behaviors have already been written, and the logic is the same in all locations. It didn't matter to the AI what pathfinding was used. The main thing is to get the way to the goal and complete your task!
For myself, I have identified two solutions. The first was to adapt the behavior to the terrain, for example, using the strategy pattern. But in this case, additional logic would have to be written for each type of navigation. The second solution was to unify the pathfinding data. With this approach, the AI did not need to be supplemented with unnecessary logic, and the search engines took over all the work!
Implementation
Main objects:
IPath <TPoint> (path data)
IPathProvider <TPoint> (search engine or path providing object)
IPathResponse <TPoint> (containing the path of the response received from the search engine)
IPathRequestToken <TPoint> (token for generating the response)
IPath
. , , , . , , Vector3 Vector2 , .
public interface IPath<TPoint>
{
// .
TPoint Current { get; }
// .
IEnumerable<TPoint> Points { get; }
// .
bool Continue(TPoint origin);
}
IPath , , , - null, , . Continue.
— . ? null? , , , .. .
public class EmptyPath<TPoint> : IPath<TPoint>
{
public TPoint Current => default(TPoint);
public IEnumerable<TPoint> Points => null;
public bool Continue(TPoint origin) => false;
}
// , .
public class EmptyPathException : Exception
{
public EmptyPathException()
: base("Path is empty! Try using EmptyPath<TPoint> instead of Path<TPoint>")
{}
}
:
public class Path<TPoint> : IPath<TPoint>
{
// .
// .
protected readonly Func<TPoint, TPoint, bool> ContinueFunc;
protected readonly IEnumerator<TPoint> PointsEnumerator;
// .
public TPoint Current { get; protected set; }
// .
public IEnumerable<TPoint> Points { get; protected set; }
// .
// .
public bool Continued { get; protected set; }
public Path(IEnumerable<TPoint> points, Func<TPoint, TPoint, bool> continueFunc)
{
// .
if(points == null)
throw new EmptyPathException();
ContinueFunc = continueFunc;
PointsEnumerator = points.GetEnumerator();
Points = points;
//
// .
MovePointer();
}
// .
public bool Continue(TPoint origin)
{
// .
if (ContinueFunc(origin, Current))
MovePointer();
// .
return Continued;
}
// ,
// .
protected void MovePointer()
{
// .
if (PointsEnumerator.MoveNext())
{
Current = PointsEnumerator.Current;
Continued = true;
}
else
{
//
Continued = false;
}
}
}
Func<TPoint, TPoint, bool> ContinueFunc — (, ). , . .
IEnumerator<TPoint> PointsEnumerator — .
Path , . : null , .
IPath . . / , .
:)
IPathProvider IPathResponse
, , .
IPathProvider<TPoint> — , , . . :
public interface IPathProvider<TPoint>
{
// , , .
IPathResponse<TPoint> RequestPath(TPoint entryPoint, TPoint endPoint);
}
:
public interface IPathResponse<TPoint>
{
// .
bool Ready { get; }
// , null.
IPath<TPoint> Path { get; }
}
IPathResponse<TPoint> Path Ready, . / true.
:
public sealed class PathResponseSync<TPoint> : IPathResponse<TPoint>
{
public bool Ready { get; private set; }
public IPath<TPoint> Path { get; private set; }
public PathResponseSync(IPath<TPoint> path)
{
if(path == null)
throw new EmptyPathException();
Path = path;
Ready = true;
}
}
, . .
. , IPathResponse .
:
public sealed class PathRequestToken<TPoint>
{
public bool IsReady { get; private set; }
public IPath<TPoint> Path { get; private set; }
public void Ready(IPath<TPoint> path)
{
if (path == null)
throw new EmptyPathException();
IsReady = true;
Path = path;
}
}
IPathResponse. , IPathResponse. , .
:
public sealed class PathResponse<TPoint> : IPathResponse<TPoint>
{
private readonly PathRequestToken<TPoint> _token;
public bool Ready => _token.IsReady;
public IPath<TPoint> Path => _token.Path;
public PathResponse(PathRequestToken<TPoint> token)
{
_token = token;
}
// .
public static void New(out PathRequestToken<TPoint> token,
out PathResponse<TPoint> response)
{
token = new PathRequestToken<TPoint>();
response = new PathResponse<TPoint>(token);
}
}
/ .
, .
, , , .
, ! : IPathResponse.
, Update :
..
private IPathProvider<Vector3> _pathProvider;
private IPathResponse<Vector3> _pathResponse;
..
public override void Update(float deltaTime)
{
// .
_pathUpdateTimer += deltaTime;
if (_pathUpdateTimer >= Owner.PathUpdateRate)
{
_pathUpdateTimer = 0f;
if (Target == null)
Target = _scanFunction(Owner);
if (Target == null)
return;
// .
_pathResponse = _pathProvider
.RequestPath(Position, Target.transform.position);
}
// , .
if (_pathResponse != null)
{
//
if (_pathResponse.Ready)
{
var path = _pathResponse.Path;
//
// .
if (path.Continue(Position))
{
// -
var nextPosition = Vector3.MoveTowards( Position, path.Current,
Owner.MovementSpeed * deltaTime);
Position = nextPosition;
}
}
}
}
:
public static bool Vector3Continuation(Vector3 origin, Vector3 current)
{
var distance = (origin - current).sqrMagnitude;
return distance <= float.Epsilon;
}
:
public IPathResponse<Vector3> RequestPath(Vector3 entryPoint, Vector3 endPoint)
{
// , ...
// LinkedAPoint.
var pathRaw = _jastar.FindPath(startPointJastar, endPointJastar);
// , .
if(pathRaw.Count == 0)
return new PathResponseSync<Vector3>(new EmptyPath<Vector3>());
var vectorList = pathRaw.ToVector3List();
// .
return new PathResponseSync<Vector3>(
new Path<Vector3>(vectorsList, PathFuncs.Vector3Continuation));
}