Unificación de la búsqueda de caminos en lugar de diferentes lógicas de IA

Para un juego sencillo, necesitaba implementar el comportamiento de la IA con una funcionalidad básica: patrullaje, persecución y combate. La tarea en sí es simple, sin embargo, había dos tipos de ubicaciones y con diferentes niveles de abstracción.





En un caso, la acción se desarrolló en espacios reducidos y en el otro, en medio de las calles de la ciudad. En espacios pequeños, se generó una cuadrícula de navegación, pero en una ubicación grande, se utilizó la búsqueda de rutas gráficas para mantener el rendimiento.





Ya se han escrito todo tipo de comportamientos y la lógica es la misma en todos los lugares. A la IA no le importaba qué pathfinding se usara. ¡Lo principal es llegar al objetivo y completar tu tarea!





Para mí, he identificado dos soluciones. El primero fue adaptar el comportamiento al terreno, por ejemplo, utilizando el patrón de estrategia. Pero en este caso, se tendría que escribir una lógica adicional para cada tipo de navegación. La segunda solución fue unificar los datos de búsqueda de caminos. Con este enfoque, la IA no necesitaba ser complementada con lógica innecesaria, ¡y los motores de búsqueda se hicieron cargo de todo el trabajo!





Implementación

Objetos principales:





  • IPath <TPoint> (datos de ruta)





  • IPathProvider <TPoint> (motor de búsqueda o objeto que proporciona la ruta)





  • IPathResponse <TPoint> (que contiene la ruta de la respuesta recibida del motor de búsqueda)





  • IPathRequestToken <TPoint> (token para generar la respuesta)





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));
}
      
      



. .








All Articles