En este proyecto, implementaremos un sistema de navegación básica para un personaje en un entorno de videojuego. Utilizaremos el algoritmo A* para la búsqueda de caminos y NavMesh para la navegación. Este proyecto consolidará los conceptos aprendidos en el Módulo 2.

Objetivos del Proyecto

  1. Implementar el algoritmo A* para la búsqueda de caminos.
  2. Integrar NavMesh para la navegación del personaje.
  3. Crear un entorno de prueba donde el personaje pueda moverse de un punto a otro evitando obstáculos.

Requisitos Previos

Antes de comenzar, asegúrate de tener instaladas las siguientes herramientas:

  • Un motor de juego (Unity o Unreal Engine recomendado).
  • Conocimientos básicos de programación en C# (para Unity) o C++ (para Unreal Engine).

Paso 1: Configuración del Entorno

Unity

  1. Crear un nuevo proyecto: Abre Unity y crea un nuevo proyecto 3D.
  2. Configurar el terreno: Añade un terreno a la escena y crea algunos obstáculos (cubos, esferas, etc.).
  3. Añadir un personaje: Importa un modelo de personaje y colócalo en la escena.

Unreal Engine

  1. Crear un nuevo proyecto: Abre Unreal Engine y crea un nuevo proyecto de tipo "Third Person".
  2. Configurar el terreno: Añade un terreno y algunos obstáculos a la escena.
  3. Añadir un personaje: Usa el personaje predeterminado del proyecto "Third Person".

Paso 2: Implementación del Algoritmo A*

Explicación del Algoritmo A*

El algoritmo A* es un algoritmo de búsqueda de caminos que encuentra la ruta más corta entre dos puntos. Utiliza una función de costo que combina el costo del camino desde el inicio hasta el nodo actual y una estimación del costo desde el nodo actual hasta el objetivo.

Pseudocódigo de A*

function A*(start, goal)
    openSet := {start}
    cameFrom := empty map

    gScore := map with default value of Infinity
    gScore[start] := 0

    fScore := map with default value of Infinity
    fScore[start] := heuristic(start, goal)

    while openSet is not empty
        current := node in openSet with lowest fScore value
        if current == goal
            return reconstruct_path(cameFrom, current)

        openSet.Remove(current)
        for each neighbor of current
            tentative_gScore := gScore[current] + dist_between(current, neighbor)
            if tentative_gScore < gScore[neighbor]
                cameFrom[neighbor] := current
                gScore[neighbor] := tentative_gScore
                fScore[neighbor] := gScore[neighbor] + heuristic(neighbor, goal)
                if neighbor not in openSet
                    openSet.Add(neighbor)

    return failure

Implementación en Unity (C#)

using System.Collections.Generic;
using UnityEngine;

public class AStarPathfinding : MonoBehaviour
{
    public Transform start;
    public Transform goal;
    public Grid grid;

    void Update()
    {
        if (Input.GetButtonDown("Fire1"))
        {
            FindPath(start.position, goal.position);
        }
    }

    void FindPath(Vector3 startPos, Vector3 targetPos)
    {
        Node startNode = grid.NodeFromWorldPoint(startPos);
        Node targetNode = grid.NodeFromWorldPoint(targetPos);

        List<Node> openSet = new List<Node>();
        HashSet<Node> closedSet = new HashSet<Node>();
        openSet.Add(startNode);

        while (openSet.Count > 0)
        {
            Node currentNode = openSet[0];
            for (int i = 1; i < openSet.Count; i++)
            {
                if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)
                {
                    currentNode = openSet[i];
                }
            }

            openSet.Remove(currentNode);
            closedSet.Add(currentNode);

            if (currentNode == targetNode)
            {
                RetracePath(startNode, targetNode);
                return;
            }

            foreach (Node neighbor in grid.GetNeighbors(currentNode))
            {
                if (!neighbor.walkable || closedSet.Contains(neighbor))
                {
                    continue;
                }

                int newMovementCostToNeighbor = currentNode.gCost + GetDistance(currentNode, neighbor);
                if (newMovementCostToNeighbor < neighbor.gCost || !openSet.Contains(neighbor))
                {
                    neighbor.gCost = newMovementCostToNeighbor;
                    neighbor.hCost = GetDistance(neighbor, targetNode);
                    neighbor.parent = currentNode;

                    if (!openSet.Contains(neighbor))
                    {
                        openSet.Add(neighbor);
                    }
                }
            }
        }
    }

    void RetracePath(Node startNode, Node endNode)
    {
        List<Node> path = new List<Node>();
        Node currentNode = endNode;

        while (currentNode != startNode)
        {
            path.Add(currentNode);
            currentNode = currentNode.parent;
        }
        path.Reverse();

        grid.path = path;
    }

    int GetDistance(Node nodeA, Node nodeB)
    {
        int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX);
        int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY);

        if (dstX > dstY)
            return 14 * dstY + 10 * (dstX - dstY);
        return 14 * dstX + 10 * (dstY - dstX);
    }
}

Implementación en Unreal Engine (C++)

#include "AStarPathfinding.h"
#include "Grid.h"
#include "Node.h"

void AAStarPathfinding::FindPath(FVector startPos, FVector targetPos)
{
    Node* startNode = Grid->NodeFromWorldPoint(startPos);
    Node* targetNode = Grid->NodeFromWorldPoint(targetPos);

    TArray<Node*> openSet;
    TSet<Node*> closedSet;
    openSet.Add(startNode);

    while (openSet.Num() > 0)
    {
        Node* currentNode = openSet[0];
        for (int i = 1; i < openSet.Num(); i++)
        {
            if (openSet[i]->fCost < currentNode->fCost || openSet[i]->fCost == currentNode->fCost && openSet[i]->hCost < currentNode->hCost)
            {
                currentNode = openSet[i];
            }
        }

        openSet.Remove(currentNode);
        closedSet.Add(currentNode);

        if (currentNode == targetNode)
        {
            RetracePath(startNode, targetNode);
            return;
        }

        for (Node* neighbor : Grid->GetNeighbors(currentNode))
        {
            if (!neighbor->bWalkable || closedSet.Contains(neighbor))
            {
                continue;
            }

            int newMovementCostToNeighbor = currentNode->gCost + GetDistance(currentNode, neighbor);
            if (newMovementCostToNeighbor < neighbor->gCost || !openSet.Contains(neighbor))
            {
                neighbor->gCost = newMovementCostToNeighbor;
                neighbor->hCost = GetDistance(neighbor, targetNode);
                neighbor->Parent = currentNode;

                if (!openSet.Contains(neighbor))
                {
                    openSet.Add(neighbor);
                }
            }
        }
    }
}

void AAStarPathfinding::RetracePath(Node* startNode, Node* endNode)
{
    TArray<Node*> path;
    Node* currentNode = endNode;

    while (currentNode != startNode)
    {
        path.Add(currentNode);
        currentNode = currentNode->Parent;
    }
    Algo::Reverse(path);

    Grid->Path = path;
}

int AAStarPathfinding::GetDistance(Node* nodeA, Node* nodeB)
{
    int dstX = FMath::Abs(nodeA->GridX - nodeB->GridX);
    int dstY = FMath::Abs(nodeA->GridY - nodeB->GridY);

    if (dstX > dstY)
        return 14 * dstY + 10 * (dstX - dstY);
    return 14 * dstX + 10 * (dstY - dstX);
}

Paso 3: Integración de NavMesh

Unity

  1. Añadir NavMesh: Ve a Window > AI > Navigation y marca el terreno como "Walkable".
  2. Añadir NavMeshAgent: Añade el componente NavMeshAgent al personaje.
  3. Script de Movimiento:
using UnityEngine;
using UnityEngine.AI;

public class NavMeshMovement : MonoBehaviour
{
    public Transform goal;

    void Start()
    {
        NavMeshAgent agent = GetComponent<NavMeshAgent>();
        agent.destination = goal.position;
    }
}

Unreal Engine

  1. Añadir NavMesh: Añade un NavMeshBoundsVolume a la escena y ajusta su tamaño para cubrir el terreno.
  2. Añadir AIController: Crea un AIController y asigna el personaje.
  3. Script de Movimiento:
#include "AIController.h"
#include "NavigationSystem.h"

void AMyAIController::BeginPlay()
{
    Super::BeginPlay();

    APawn* ControlledPawn = GetPawn();
    if (ControlledPawn)
    {
        UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
        if (NavSystem)
        {
            NavSystem->SimpleMoveToLocation(this, GoalLocation);
        }
    }
}

Paso 4: Prueba y Depuración

  1. Ejecutar el Proyecto: Ejecuta el proyecto y observa cómo el personaje se mueve hacia el objetivo evitando obstáculos.
  2. Depuración: Si el personaje no se mueve correctamente, revisa los componentes de NavMesh y asegúrate de que el terreno y los obstáculos estén configurados correctamente.

Ejercicio Práctico

Tarea

Modifica el proyecto para que el personaje pueda moverse a múltiples objetivos en secuencia. Implementa una lista de objetivos y haz que el personaje se mueva a cada uno en orden.

Solución

using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;

public class MultiTargetNavMeshMovement : MonoBehaviour
{
    public List<Transform> goals;
    private int currentGoalIndex = 0;
    private NavMeshAgent agent;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        MoveToNextGoal();
    }

    void Update()
    {
        if (!agent.pathPending && agent.remainingDistance < 0.5f)
        {
            MoveToNextGoal();
        }
    }

    void MoveToNextGoal()
    {
        if (goals.Count == 0)
            return;

        agent.destination = goals[currentGoalIndex].position;
        currentGoalIndex = (currentGoalIndex + 1) % goals.Count;
    }
}

Conclusión

En este proyecto, hemos implementado un sistema de navegación básica utilizando el algoritmo A* y NavMesh. Hemos aprendido a configurar el entorno, implementar el algoritmo de búsqueda de caminos y utilizar NavMesh para la navegación del personaje. Este conocimiento es fundamental para desarrollar comportamientos inteligentes en los personajes de videojuegos.

En el siguiente proyecto, aplicaremos estos conceptos para crear un NPC con toma de decisiones.

© Copyright 2024. Todos los derechos reservados