1つの簡単なゲームでは、基本的な機能(パトロール、追跡、戦闘)を使用してAIの動作を実装する必要がありました。タスク自体は単純ですが、場所には2つのタイプがあり、抽象化のレベルが異なります。
あるケースでは、アクションは限られたスペースで行われ、別のケースでは、街の通りの真ん中で行われました。小さなスペースではナビゲーショングリッドが生成されましたが、大きな場所では、パフォーマンスを維持するためにグラフパスファインディングが使用されました。
あらゆる種類の動作がすでに記述されており、ロジックはすべての場所で同じです。どのパスファインディングが使用されたかはAIにとって重要ではありませんでした。主なことは、目標に到達し、タスクを完了することです!
私自身、2つの解決策を特定しました。1つ目は、たとえば戦略パターンを使用して、動作を地形に適応させることでした。ただし、この場合、ナビゲーションのタイプごとに追加のロジックを作成する必要があります。2番目の解決策は、経路探索データを統合することでした。このアプローチでは、AIに不要なロジックを追加する必要はなく、検索エンジンがすべての作業を引き継ぎました!
実装
主な目的:
IPath <TPoint>(パスデータ)
IPathProvider <TPoint>(検索エンジンまたはパス提供オブジェクト)
IPathResponse <TPoint>(検索エンジンから受信した応答のパスを含む)
IPathRequestToken <TPoint>(応答を生成するためのトークン)
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));
}