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
- Implementar el algoritmo A* para la búsqueda de caminos.
- Integrar NavMesh para la navegación del personaje.
- 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
- Crear un nuevo proyecto: Abre Unity y crea un nuevo proyecto 3D.
- Configurar el terreno: Añade un terreno a la escena y crea algunos obstáculos (cubos, esferas, etc.).
- Añadir un personaje: Importa un modelo de personaje y colócalo en la escena.
Unreal Engine
- Crear un nuevo proyecto: Abre Unreal Engine y crea un nuevo proyecto de tipo "Third Person".
- Configurar el terreno: Añade un terreno y algunos obstáculos a la escena.
- 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
- Añadir NavMesh: Ve a
Window > AI > Navigation
y marca el terreno como "Walkable". - Añadir NavMeshAgent: Añade el componente
NavMeshAgent
al personaje. - 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
- Añadir NavMesh: Añade un
NavMeshBoundsVolume
a la escena y ajusta su tamaño para cubrir el terreno. - Añadir AIController: Crea un
AIController
y asigna el personaje. - 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
- Ejecutar el Proyecto: Ejecuta el proyecto y observa cómo el personaje se mueve hacia el objetivo evitando obstáculos.
- 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.
IA para Videojuegos
Módulo 1: Introducción a la IA en Videojuegos
- Historia y Evolución de la IA en Videojuegos
- Conceptos Básicos de IA
- Herramientas y Lenguajes de Programación
Módulo 2: Navegación en Videojuegos
- Algoritmos de Búsqueda de Caminos
- Implementación de A*
- Navegación con NavMesh
- Evitación de Obstáculos
Módulo 3: Toma de Decisiones
Módulo 4: Aprendizaje Automático
- Introducción al Aprendizaje Automático
- Redes Neuronales en Videojuegos
- Aprendizaje por Refuerzo
- Implementación de un Agente de Aprendizaje
Módulo 5: Integración y Optimización
Módulo 6: Proyectos Prácticos
- Proyecto 1: Implementación de Navegación Básica
- Proyecto 2: Creación de un NPC con Toma de Decisiones
- Proyecto 3: Desarrollo de un Agente con Aprendizaje Automático