Pour un jeu simple, je devais implémenter le comportement de l'IA avec des fonctionnalités de base: patrouilles, poursuite et combat. La tâche elle-même est simple, cependant, il y avait deux types de lieux et avec différents niveaux d'abstraction.
Dans un cas, l'action s'est déroulée dans des espaces confinés, et dans l'autre, au milieu des rues de la ville. Dans les petits espaces, une grille de navigation a été générée, mais dans un grand emplacement, la recherche de chemin de graphique a été utilisée pour maintenir les performances.
Toutes sortes de comportements ont déjà été écrits et la logique est la même dans tous les endroits. Peu importe pour l'IA le cheminement utilisé. L'essentiel est d'arriver au but et d'accomplir votre tâche!
Pour moi, j'ai identifié deux solutions. La première était d'adapter le comportement au terrain, par exemple, en utilisant le modèle de stratégie. Mais dans ce cas, une logique supplémentaire devrait être écrite pour chaque type de navigation. La deuxième solution consistait à unifier les données de recherche de chemin. Avec cette approche, l'IA n'avait pas besoin d'être complétée par une logique inutile, et les moteurs de recherche ont repris tout le travail!
Mise en œuvre
Objets principaux:
IPath <TPoint> (données de chemin)
IPathProvider <TPoint> (moteur de recherche ou objet fournissant le chemin)
IPathResponse <TPoint> (contenant le chemin de la réponse reçue du moteur de recherche)
IPathRequestToken <TPoint> (jeton pour générer la réponse)
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));
}