3DIAInteligencia ArtificialNiixerRealidad MixtaSketchfab

Desarrollo de un videojuego en Realidad Mixta con Unity e Inteligencia Artificial

¿Cómo se Desarrollo un videojuego en Realidad Mixta con Unity?

Este artículo presenta el desarrollo de un videojuego de disparos en realidad mixta (MR) diseñado para las gafas Microsoft HoloLens 1. El juego se compone de tres niveles con dificultad progresiva, integrando elementos de inteligencia artificial, interacción espacial, y diseño inmersivo. A continuación, se describe el proceso de diseño, implementación y despliegue.

¿Qué son las HoloLens y la realidad mixta?
Las Microsoft HoloLens son un dispositivo de computación holográfica que combina realidad aumentada con capacidades avanzadas de sensores y procesamiento espacial, permitiendo superponer elementos digitales interactivos sobre el entorno físico del usuario. Este tipo de tecnología se enmarca dentro de lo que se conoce como realidad mixta (MR, por sus siglas en inglés), la cual integra el mundo real y virtual para permitir interacciones en tiempo real entre objetos físicos y digitales. A diferencia de la realidad aumentada (que solo añade elementos visuales) o la realidad virtual (que aísla al usuario en un entorno simulado), la realidad mixta permite que los objetos digitales respondan a las superficies, obstáculos y movimientos del mundo real, ofreciendo experiencias inmersivas y funcionales para juegos, educación, medicina e ingeniería.

1.Planteamiento del Proyecto

La idea central del proyecto fue desarrollar un shooter inmersivo en realidad mixta en el que el jugador pudiera interactuar directamente con su entorno físico, integrando elementos del mundo real con experiencias digitales dinámicas. El objetivo principal del juego es eliminar enemigos virtuales que se desplazan inteligentemente por el espacio disponible en la habitación del usuario, aprovechando las capacidades de mapeo espacial de las HoloLens 1.

Para aumentar el nivel de desafío y mantener el interés del jugador, se diseñaron tres niveles progresivos, en los cuales la dificultad incrementa no solo por la cantidad de enemigos, sino principalmente por el aumento en la velocidad de desplazamiento de estos, así como por su comportamiento más reactivo al entorno. Cada vez que un enemigo logra acercarse al jugador y establecer contacto, se produce una reducción en la salud del jugador, simulando daño, lo cual obliga al usuario a mantenerse en constante movimiento y alerta.

Una referencia clave en el diseño conceptual del juego fue el título Drop Dead: The Cabin – Home Mixed Reality, un juego desarrollado para plataformas avanzadas de realidad mixta que combina acción en primera persona con navegación espacial y mecánicas de combate intuitivas.

2.Tecnología Utilizada

Se utilizó el motor Unity 2021.3.45f LTS, una versión robusta y estable, ideal para realidad mixta por su compatibilidad con las herramientas diseñadas para HoloLens. Desde las etapas iniciales del desarrollo del videojuego, se empleó el emulador de Microsoft HoloLens 1 para simular el comportamiento del entorno real y validar la experiencia de usuario sin necesidad de recurrir constantemente al hardware físico. Además, la lógica del videojuego fue completamente programada en C#, el lenguaje nativo de Unity, lo cual facilitó enormemente el desarrollo del videojuego al integrar componentes clave como físicas, navegación espacial e interacciones gestuales de forma fluida y modular.

Para la inteligencia artificial de los enemigos se implementó el sistema NavMesh de Unity, que permite definir áreas navegables y generar rutas dinámicas dentro del entorno físico del jugador. Los modelos 3D de enemigos y partes del escenario fueron obtenidos desde plataformas como Sketchfab y BlenderKit, seleccionando recursos con licencias adecuadas y optimizándolos para su uso en realidad mixta. Finalmente, la interfaz de usuario y la lógica de progresión entre niveles se desarrollaron con Unity Canvas, diseñados para flotar en el espacio y ser activados mediante gestos, lo que garantiza una experiencia interactiva completamente inmersiva y natural.

  • Dispositivo objetivo: Emulador de Microsoft HoloLens 1
  • Lenguaje de programación: C#
  • IA y navegación: Sistema de NavMesh de Unity
  • Modelos 3D: Sketchfab y BlenderKit
  • Interfaz y progresión: Unity Canvas
  • Motor de juego: Unity 2021.3.45f LTS

Con la version de unity 2021.3.45f1 creamos un nueva Scene

Realizamos la descarga de esta herramienta, esta herramienta va a descargar los paquetes para realidad mixta.

seleccionamos estas opciones y le damos en descargar

seleccionamos estas otras opciones y le damos siguiente

una vez seleccionado  la carpeta del proyecto seleccionamos estas opciones

  • Mixed Reality Toolkit Foundation (el núcleo del sistema MRTK)
  • Mixed Reality Toolkit Examples (opcional pero útil para probar)
  • Mixed Reality Toolkit Extensions (añade funciones extra como conectores)
  • Mixed Reality Toolkit Tools (para utilidades y ajustes en escena)

3. Diseño de la Jugabilidad

Configuración de Enemigos y Componentes de Movimiento

Cada modelo de enemigo fue programado individualmente con sus propios parámetros de comportamiento y combate. Todos ellos cuentan con el componente NavMesh Agent de Unity, lo que les permite moverse de forma autónoma dentro del entorno real delimitado, siguiendo rutas dinámicas hacia el jugador.

A cada enemigo se le asignaron las siguientes propiedades:

  • Vida: Define cuántos impactos puede recibir antes de ser eliminado.
  • Velocidad de movimiento: Controla qué tan rápido se desplaza el enemigo hacia el jugador. Este valor se ajusta según el nivel de dificultad.
  • Velocidad de ataque: Determina la frecuencia con la que el enemigo puede causar daño una vez está dentro del rango del jugador.
  • Rango de ataque: Es la distancia mínima requerida entre el enemigo y el jugador para que se ejecute una acción ofensiva.

Animaciones y Comportamiento de los Enemigos

Cada tipo de enemigo del juego cuenta con un conjunto específico de animaciones y comportamientos, lo que aporta diversidad visual y mejora la experiencia inmersiva del jugador. Las animaciones principales fueron obtenidas desde la plataforma Mixamo, aplicándolas individualmente a los modelos compatibles.

  • Galleta y Pizza: Estos enemigos cuentan con tres animaciones principales:
    • Correr: Se activa cuando detectan al jugador y se dirigen hacia él.
    • Movimiento base: Utilizada para patrullar o desplazarse sin detectar al jugador.
    • Ataque: Se ejecuta al entrar en contacto con el jugador, simulando un golpe o embestida.
  • Fantasmas: A diferencia de los otros enemigos, los fantasmas no poseen animaciones importadas de Mixamo. Su comportamiento es más etéreo:
    • Se desplazan en línea recta hacia el jugador de forma continua.
    • Realizan pequeñas pausas o tambaleos, generando una sensación de inestabilidad o levitación.
    • Al morir, simplemente desaparecen sin animación de muerte.

Independientemente del tipo de enemigo, todos emiten un efecto de sonido característico al morir, lo que refuerza el feedback auditivo del juego y permite al jugador percibir claramente cuándo ha derrotado a un oponente.

Árbol de Animaciones y Transiciones

El comportamiento animado de los personajes galleta y pizza se gestiona mediante un Animator Controller en Unity, estructurado como un árbol de estados. Este árbol define los diferentes estados de animación y las transiciones entre ellos, controladas por condiciones lógicas que se evalúan en tiempo real durante el juego.

Estados principales:

  • Idle/Movimiento base: Estado por defecto cuando el enemigo está activo pero aún no ha detectado al jugador.
  • Correr: Se activa cuando el enemigo ha detectado al jugador y comienza a perseguirlo.
  • Atacar: Se ejecuta cuando el enemigo entra en el rango de ataque.
  • Morir: Se activa cuando la vida del enemigo llega a cero.

Transiciones:

Las flechas que conectan los estados representan las condiciones necesarias para cambiar de uno a otro. Por ejemplo:

  • De Idle a Correr si el jugador está en rango de detección.
  • De Correr a Atacar si la distancia al jugador es menor al rango de ataque.
  • De cualquier estado a Morir si la vida del enemigo es igual a cero.

Cada transición está acompañada de un efecto de sonido específico, que refuerza la acción (por ejemplo, gruñidos al atacar y efectos al morir), generando una experiencia audiovisual más inmersiva. Cabe aclarar que los fantasmas no utilizan este sistema, ya que no cuentan con animaciones complejas, sino con un movimiento simple y desaparición al ser derrotados.

Script1: Base de Comportamiento para Enemigos

El primer script desarrollado para los enemigos actúa como componente base del comportamiento general que comparten todos los tipos de enemigos en el juego. Este script centraliza las funciones esenciales para la interacción con el jugador y la toma de decisiones básicas dentro del entorno de realidad mixta.

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

#if UNITY_EDITOR
using UnityEditor;
#endif

public class Enemigo : MonoBehaviour
{
    public Estados estado;
    public float distanciaSeguir;
    public float distanciaAtacar;
    public float distanciaEscapar;
    public bool autoseleccionarTarget = true;
    public Transform target;

    public float distancia;
    public bool vivo = true;

    protected virtual void Awake()
    {
        if (autoseleccionarTarget)
        {
            GameObject player = GameObject.FindGameObjectWithTag("Player");
            if (player != null)
                target = player.transform;
        }

        StartCoroutine(CalcularDistancia());
    }

    private void LateUpdate()
    {
        if (vivo && target != null)
            CheckEstado();
    }

    private void CheckEstado()
    {
        switch (estado)
        {
            case Estados.idle:
                EstadoIdle();
                break;
            case Estados.seguir:
                EstadoSeguir();
                break;
            case Estados.atacar:
                EstadoAtacar();
                break;
            case Estados.muerto:
                Estadomuerto();
                break;
        }
    }

    public void CambiarEstado(Estados e)
    {
        switch (e)
        {
            case Estados.muerto:
                vivo = false;
                break;
        }

        estado = e;
    }

    public virtual void EstadoIdle()
    {
        if (distancia < distanciaSeguir)
            CambiarEstado(Estados.seguir);
    }

    public virtual void EstadoSeguir()
    {
        if (distancia < distanciaAtacar)
            CambiarEstado(Estados.atacar);
        else if (distancia > distanciaEscapar)
            CambiarEstado(Estados.idle);
    }

    public virtual void EstadoAtacar()
    {
        if (distancia > distanciaAtacar + 0.4f)
            CambiarEstado(Estados.seguir);
    }

    public virtual void Estadomuerto()
    {
        // Por defecto no hace nada. Puede sobreescribirse.
    }

    IEnumerator CalcularDistancia()
    {
        while (vivo)
        {
            if (target != null)
                distancia = Vector3.Distance(transform.position, target.position);

            yield return new WaitForSeconds(0.3f);
        }
    }

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        Handles.color = Color.red;
        Handles.DrawWireDisc(transform.position, Vector3.up, distanciaAtacar);

        Handles.color = Color.yellow;
        Handles.DrawWireDisc(transform.position, Vector3.up, distanciaSeguir);

        Handles.color = Color.green;
        Handles.DrawWireDisc(transform.position, Vector3.up, distanciaEscapar);
    }
#endif

    private void OnDrawGizmos()
    {
        int icono = (int)estado;
        icono++;
        Gizmos.DrawIcon(transform.position + Vector3.up * 1.2f, "@" + icono + ".png", false);
    }

    public enum Estados
    {
        idle = 0,
        seguir = 1,
        atacar = 2,
        muerto = 3
    }
}

Script2: Comportamiento Específico con Navegación Inteligente

El script extiende la funcionalidad general definida en la clase base Enemigo, proporcionando una implementación específica para enemigos que requieren movimiento controlado mediante el componente NavMeshAgent de Unity. Esta clase se encarga de ejecutar acciones contextuales como perseguir al jugador, detenerse al atacar y desactivarse al morir, todo dentro del entorno de realidad mixta.

Principales funcionalidades del script:

  • Inicialización del NavMeshAgent
    En el método Awake(), se obtiene una referencia al componente NavMeshAgent, que es obligatorio gracias al atributo [RequireComponent(typeof(NavMeshAgent))]. Esto garantiza que el GameObject tenga siempre el componente necesario para navegar por el espacio.
  • Estado Idle (reposo)
    El enemigo se detiene completamente y no se dirige a ningún destino: csharpCopiarEditaragente.SetDestination(transform.position);
  • Estado Seguir (persecución)
    Cuando el jugador está dentro del rango de detección, el enemigo se activa y comienza a seguirlo: csharpCopiarEditaragente.SetDestination(target.position);
  • Estado Atacar
    El enemigo se detiene al alcanzar el rango de ataque y rota hacia el jugador usando transform.LookAt, manteniendo la coherencia visual y estratégica: csharpCopiarEditaragente.SetDestination(transform.position); transform.LookAt(target, Vector3.up);
  • Estado Muerto
    Se desactiva el NavMeshAgent para detener todo movimiento al morir: csharpCopiarEditaragente.enabled = false;

Cómo implementar este script en Unity

  1. Preparación del terreno
    Asegúrate de que el entorno donde se moverán los enemigos tiene un plano o superficie que pueda actuar como malla de navegación.
  2. Hornear (bakear) el NavMesh
    • Ve al menú Window > AI > Navigation.
    • Selecciona los objetos del entorno (por ejemplo, el suelo) y márcalos como Navigation Static.
    • En la pestaña Bake, ajusta los parámetros (altura, radio, pendiente máxima) y presiona Bake para generar el NavMesh.
  3. Configuración del enemigo
    • Asigna al GameObject del enemigo el componente NavMeshAgent (si no se ha agregado automáticamente).
    • Añade el script Enemigo2 al GameObject.
    • Define un Transform objetivo (por ejemplo, el jugador).
    • Configura los valores de velocidad, rango de ataque y estados desde el inspector o código.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class Enemigo2 : Enemigo
{
    private NavMeshAgent agente;

    protected override void Awake()
    {
        base.Awake();
        agente = GetComponent<NavMeshAgent>();
    }

    public override void EstadoIdle()
    {
        base.EstadoIdle();
        if (agente.enabled)
            agente.SetDestination(transform.position);
    }

    public override void EstadoSeguir()
    {
        base.EstadoSeguir();
        if (agente.enabled && target != null)
            agente.SetDestination(target.position);
    }

    public override void EstadoAtacar()
    {
        base.EstadoAtacar();
        if (agente.enabled)
        {
            agente.SetDestination(transform.position);
            transform.LookAt(target, Vector3.up);
        }
    }

    public override void Estadomuerto()
    {
        base.Estadomuerto();
        if (agente != null)
            agente.enabled = false;
    }
}

Script: Manejo de Física y Colisión con Proyectiles

El script Enemy implementa la lógica de respuesta física cuando un enemigo es alcanzado por una bala (objeto tipo Sphere). Este comportamiento se encarga de simular una reacción realista en el enemigo al momento del impacto, añadiendo un efecto visual y físico antes de su destrucción.

Funcionalidad principal

Al detectar una colisión mediante el método OnCollisionEnter, el script verifica si el objeto que ha impactado al enemigo tiene un nombre que contiene la palabra "Sphere" (nombre asignado comúnmente a las balas en el juego). Si se cumple esta condición:

  1. Se obtiene el componente Rigidbody del enemigo, que normalmente está configurado como cinemático para permitir control por código sin interferencia de la física de Unity. csharpCopiarEditarRigidbody rb = GetComponent<Rigidbody>();
  2. Se activa la física del enemigo al desactivar isKinematic y habilitar la gravedad: csharpCopiarEditarrb.isKinematic = false; rb.useGravity = true;
  3. Se destruye el enemigo con un retardo de 1 segundo, permitiendo que la animación física tenga tiempo de ejecutarse (por ejemplo, que el enemigo caiga o colapse): csharpCopiarEditarDestroy(gameObject, 1f);

Este enfoque mejora la sensación de impacto en el juego, ofreciendo una respuesta visual satisfactoria cuando el jugador acierta un disparo.

using UnityEngine;

public class Enemy : MonoBehaviour
{
    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.name.Contains("Sphere"))
        {
            Rigidbody rb = GetComponent<Rigidbody>();
            rb.isKinematic = false;
            rb.useGravity = true;

            Destroy(gameObject, 1f); // se destruye con retardo
        }
    }
}

Script: Control de Instancia Única (Singleton)

La clase Personaje implementa el patrón de diseño singleton, lo cual garantiza que solo exista una instancia activa de este objeto en la escena en un momento dado. Este patrón es especialmente útil cuando se quiere tener un enemigo especial, jefe o entidad única que no debe ser duplicada accidentalmente durante el juego o la carga de escenas.

Funcionalidad principal

  • Propiedad estática singleton:
    Se utiliza para almacenar la única instancia permitida del objeto Personaje.
  • Método Awake():
    Este método se ejecuta al iniciar el objeto en la escena. Su lógica es:
    • Si aún no existe una instancia (singleton == null), esta instancia se registra como la única.
    • Si ya existe una instancia activa, la nueva se destruye inmediatamente usando DestroyImmediate, eliminando cualquier duplicado no deseado: csharpCopiarEditarDestroyImmediate(this.gameObject);
  • Referencia a componente Vida:
    La clase también mantiene una referencia pública al componente Vida, lo cual puede ser útil si se desea controlar la salud de esta entidad especial de forma global.
using UnityEngine;
using Microsoft.MixedReality.Toolkit;
using Microsoft.MixedReality.Toolkit.Input;

public class Shooter : MonoBehaviour, IMixedRealityPointerHandler
{
    public GameObject bulletPrefab;
    public float bulletSpeed = 10f;
    public int health = 5;
    public AudioSource dmgSound;
    void Start()
    {
        CoreServices.InputSystem?.RegisterHandler<IMixedRealityPointerHandler>(this);
    }

    void OnDestroy()
    {
        CoreServices.InputSystem?.UnregisterHandler<IMixedRealityPointerHandler>(this);
    }

    public void OnPointerClicked(MixedRealityPointerEventData eventData)
    {
        Fire();
    }

    void Fire()
    {
        Debug.Log("¡Disparo!");
        GameObject bullet = Instantiate(bulletPrefab, Camera.main.transform.position, Quaternion.identity);
        Rigidbody rb = bullet.GetComponent<Rigidbody>();
        bullet.GetComponent<Renderer>().material.color = Color.red;
        rb.velocity = Camera.main.transform.forward * bulletSpeed;
        Destroy(bullet, 5f);
        transform.GetComponent<AudioSource>().Play();
    }

    public void OnDamage()
    {
        dmgSound.Play();
    }

    public void OnPointerDown(MixedRealityPointerEventData eventData) { }
    public void OnPointerDragged(MixedRealityPointerEventData eventData) { }
    public void OnPointerUp(MixedRealityPointerEventData eventData) { }
}

Sistema de Spawners y Diseño de Oleadas

El sistema de spawners fue diseñado para controlar de forma estratégica la aparición de enemigos en cada nivel del juego. Estos spawners son puntos definidos en el mapa desde donde emergen los enemigos, y su distribución varía según el nivel para aumentar gradualmente la dificultad.

Cada nivel cuenta con un conjunto específico de spawners ubicados en distintas posiciones del entorno virtual, lo que permite:

  • Generar oleadas más impredecibles.
  • Obligar al jugador a desplazarse constantemente y mantener la atención en múltiples direcciones.
  • Controlar el ritmo y la intensidad del combate en función del progreso del jugador.

Al avanzar de nivel, se incrementa:

  • El número de spawners activos.
  • La frecuencia de aparición de enemigos.
  • La complejidad del patrón de ataque, ya que los enemigos pueden surgir desde ángulos inesperados.

Este sistema permite diseñar una experiencia escalonada y dinámica, haciendo que cada nivel represente un nuevo reto espacial y estratégico dentro del entorno de realidad mixta.

Script:Sistema de Spawners y Diseño de Oleadas

using System.Collections;
using UnityEngine;

public class SpawnerEnemigos : MonoBehaviour
{
    [SerializeField] private GameObject[] enemigos; // Prefabs
    [SerializeField] private float tiempoMin = 1f;
    [SerializeField] private float tiempoMax = 3f;

    [SerializeField] private Transform puntoSpawn => transform;

    private GameManager gameManager;

    private void Start()
    {
        gameManager = GameManager.Instance;
        StartCoroutine(SpawnLoop());
    }

    private IEnumerator SpawnLoop()
    {
        while (!gameManager.NivelFinalizado)
        {
            SpawnEnemigo();
            float tiempoAleatorio = Random.Range(tiempoMin, tiempoMax);
            yield return new WaitForSeconds(tiempoAleatorio);
        }
    }

    private void SpawnEnemigo()
    {
        int index = Random.Range(0, enemigos.Length);
        GameObject enemigoGO = Instantiate(enemigos[index], puntoSpawn.position, Quaternion.identity);
        EnemigoBase enemigo = enemigoGO.GetComponent<EnemigoBase>();
        enemigo.SetJugador(gameManager.Jugador);

        enemigoGO.AddComponent<EnemigoReportador>().Init(gameManager);
    }
}

Script: Gestión de Salud y Evento de Muerte

El script Vida se encarga de gestionar el sistema de salud de los enemigos u otros objetos dañables en el juego. Esta clase es modular y puede ser reutilizada para diferentes personajes, ya que define la vida inicial, el daño recibido y un evento personalizado que se ejecuta cuando el objeto muere.

Funcionalidades principales:

  • Inicialización de vida:
    En el método Start(), se asigna a vidaActual el valor definido en vidaInicial, permitiendo configurar fácilmente la resistencia de cada enemigo desde el editor de Unity.
  • Método CausarDaño(float cuanto):
    Este método reduce la vida actual en función del daño recibido (cuanto). Si la vida llega a cero o menos:
    • Se imprime un mensaje en consola con el nombre del objeto muerto.
    • Se ejecuta el eventoMorir mediante eventoMorir.Invoke().
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Vida : MonoBehaviour
{
    public float vidaInicial;
    public float vidaActual;
    public UnityEvent eventoMorir;  // Evento que se dispara al morir

    void Start()
    {
        vidaActual = vidaInicial;
    }

    public void CausarDaño(float cuanto)
    {
        vidaActual -= cuanto;
        if (vidaActual <= 0)
        {
            print("Muerto!! -> " + gameObject.name);
            eventoMorir.Invoke();  // Dispara el evento asociado
        }
    }
}

Script : Enemigo Derivado con Comportamiento Especializado

La clase Dulce representa un tipo de enemigo dentro del videojuego y hereda de la clase abstracta EnemigoBase, lo que permite estructurar el comportamiento común de todos los enemigos y especializarlo para cada caso. Esta clase se encarga de definir características específicas como velocidad de movimiento, velocidad de ataque y el tipo de acción que ejecuta al alcanzar al jugador.

Herencia y estructura

Dulce sobrescribe métodos abstractos definidos en EnemigoBase, estableciendo sus propios valores y animaciones. Esto permite una arquitectura de programación orientada a objetos clara y escalable, ya que nuevos enemigos pueden crearse fácilmente heredando la misma base.

Funcionalidades clave:

  • Velocidades personalizadas:
    • GetVelocidad(): Define la velocidad de movimiento del enemigo (1.5 unidades por defecto).
    • GetVelocidadAtaque(): Controla la frecuencia con la que puede atacar al jugador (0.5 segundos por intento).
  • Animación de ataque y daño al jugador:
    • EjecutarAtaque(): Al entrar en rango, se llama a la función de daño desde el GameManager, restando salud al jugador, y se dispara la animación correspondiente: csharpCopiarEditarGameManager.Instance.RecibirDaño(1); animator.SetTrigger("Atacar");
  • Inicialización del Animator:
    Se asigna el Animator serializado en Unity al campo heredado para controlar las transiciones de animaciones, como correr, patrullar o atacar.

Mecánicas básicas

  • Disparo: Activado mediante gesto de air-tap.
  • Daño recibido: Cuando un enemigo entra en contacto con el jugador.
  • Progresión: Canvas del botón de reset y avanzar al siguiente nivel automáticamente.

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

public class Dulce : EnemigoBase
{
[SerializeField] private float velocidad = 1.5f;
[SerializeField] private float velocidadAtaque = 0.5f;
[SerializeField] private Animator Animator;

public GameObject player;

private void Start()
{
    base.Start(); 
    base.animator = Animator;
}

protected override float GetVelocidad()
{
    return velocidad;
}

protected override float GetVelocidadAtaque()
{
    return velocidadAtaque;
}

protected override void EjecutarAtaque()
{
    GameManager.Instance.RecibirDaño(1); 
    animator.SetTrigger("Atacar");
}

Script: Estructura General de Comportamiento para Enemigos

La clase abstracta EnemigoBase define el comportamiento genérico de todos los enemigos del juego. Actúa como una plantilla base que puede ser extendida por clases concretas como Dulce, Pizza o Fantasma, permitiendo personalizar aspectos específicos como velocidad, tipo de ataque o reacciones al daño.

Características principales

  • Gestión de movimiento y ataque
    El enemigo rastrea al jugador constantemente. Si se encuentra fuera del rango de ataque, se desplaza hacia él usando el método MoverHaciaJugador(). Si entra en el rango definido (rangoAtaque), se detiene y comienza una corrutina de ataque periódico.
  • Referencia al jugador
    Se establece con SetJugador(Transform nuevoJugador), permitiendo asignar dinámicamente el objetivo desde el GameManager u otros controladores globales.
  • Sistema de animaciones
    Mediante el componente Animator, se controla el cambio de estados visuales:
    • Movimiento: ajusta el parámetro VelocidadMovimiento.
    • Ataque: activa/desactiva Atacando.
    • Muerte: se lanza el trigger Death.
  • Sistema de daño y salud
    El método RecibirDaño(int cantidad) reduce la vida actual. Al llegar a cero, se activa el método Morir(), el cual detiene todas las corrutinas y reproduce la animación y sonido de muerte, seguido de la destrucción del objeto.
  • Colisión con balas
    El método OnTriggerEnter(Collider other) detecta colisiones con objetos etiquetados como "Bullet", aplica daño y destruye la bala: csharpCopiarEditarif (other.CompareTag("Bullet")) { RecibirDaño(1); Destroy(other.gameObject); }
  • Métodos abstractos
    Las funciones GetVelocidad(), GetVelocidadAtaque() y EjecutarAtaque() son abstractas y deben ser implementadas en cada subclase concreta para definir su comportamiento único.
using UnityEngine;
using System.Collections;

public abstract class EnemigoBase : MonoBehaviour
{
    protected Transform jugador;

    [SerializeField] protected float rangoAtaque = 1.5f;
    protected bool atacando = false;
    protected Animator animator;
    [SerializeField] protected int vidaMaxima = 3;
    protected int vidaActual;
    

    public bool Atacando
    {
        get => atacando;
        set => atacando = value;
    }
    protected virtual void Start()
    {
        vidaActual = vidaMaxima;
    }
    public void SetJugador(Transform nuevoJugador)
    {
        jugador = nuevoJugador;
    }

    protected virtual void Update()
    {
        if (jugador == null) return;

        float distancia = Vector3.Distance(transform.position, jugador.position);

        if (distancia > rangoAtaque)
        {
            atacando = false;
            StopAllCoroutines(); 
            MoverHaciaJugador();
        }
        else
        {
            if (!atacando)
            {
                atacando = true;

                if (animator != null)
                {
                    animator.SetFloat("VelocidadMovimiento", 0f);
                    animator.SetBool("Atacando", true);
                }

                StartCoroutine(AtacarJugador());
            }
        }
    }

    protected virtual void MoverHaciaJugador()
    {
        Vector3 direccion = (jugador.position - transform.position).normalized;
        transform.position += direccion * GetVelocidad() * Time.deltaTime;

        transform.LookAt(new Vector3(jugador.position.x, transform.position.y, jugador.position.z));

        if (animator != null)
        {
            animator.SetFloat("VelocidadMovimiento", GetVelocidad());
            animator.SetBool("Atacando", false);
        }
    }

    protected virtual IEnumerator AtacarJugador()
    {
        while (atacando)
        {
            EjecutarAtaque();
            yield return new WaitForSeconds (GetVelocidadAtaque());
        }
    }
    public virtual void RecibirDaño(int cantidad)
    {
        if (vidaActual <= 0) return; 

        vidaActual -= cantidad;

        if (vidaActual <= 0)
        {
            vidaActual = 0;
            Morir();
        }
    }
    protected virtual void Morir()
    {
        Debug.Log($"{name} ha muerto.");
        StopAllCoroutines();
        enabled = false;

        if (animator != null)
        {
            animator.SetFloat("VelocidadMovimiento", 0f);
            animator.SetBool("Atacando", false);
            animator.SetTrigger("Death");

            StartCoroutine(EsperarAnimacionMuerte());
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    protected virtual IEnumerator EsperarAnimacionMuerte()
    {
        transform.GetComponent<AudioSource>().Play();
        yield return new WaitForSeconds(3f); 
        Destroy(gameObject);
    }
    
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Bullet"))
        {
            RecibirDaño(1);
            Destroy(other.gameObject); 
        }
    }
    protected abstract float GetVelocidad();         
    protected abstract float GetVelocidadAtaque();   
    protected abstract void EjecutarAtaque();       
}

Script: Comunicación con el GameManager

El script EnemigoReportador cumple una función clave en la lógica de control de progresión del juego: notificar al GameManager cuando un enemigo es destruido, lo que permite llevar un registro preciso de cuántos enemigos han sido eliminados en el nivel actual.

Funcionalidad principal

  • Inicialización del GameManager
    A través del método público Init(GameManager gm), se establece una referencia al GameManager, que es el controlador central del juego. csharpCopiarEditarpublic void Init(GameManager gm) { gameManager = gm; }
  • Notificación al destruirse
    Cuando el objeto que contiene este script es destruido (por ejemplo, al morir el enemigo), se ejecuta OnDestroy(). En este método se verifica:
    • Que exista una referencia válida al GameManager.
    • Que el nivel esté en curso (NivelIniciado == true).
    Si ambas condiciones se cumplen, se llama a gameManager.ReportarMuerteEnemigo() para registrar la muerte del enemigo: csharpCopiarEditarif (gameManager != null && gameManager.NivelIniciado) { gameManager.ReportarMuerteEnemigo(); }

Uso típico en el juego

  • Este script se adjunta a cada enemigo que debe ser contado en el sistema de oleadas o progreso por nivel. Al hacerlo, el GameManager puede:
  • Mostrar mensajes o indicadores como “Nivel completado” mediante Canvas.
  • Llevar el conteo de enemigos eliminados.
  • Activar el siguiente evento, oleada o nivel al alcanzar una cantidad específica de muertes.
using UnityEngine;

public class EnemigoReportador : MonoBehaviour
{
    private GameManager gameManager;

    public void Init(GameManager gm)
    {
        gameManager = gm;
    }

    private void OnDestroy()
    {
        if (gameManager != null && gameManager.NivelIniciado)
        {
            gameManager.ReportarMuerteEnemigo();
        }
    }
}

4. Diseño de los Enemigos y Navegación Inteligente

El diseño de los enemigos en el videojuego combina tanto elementos visuales llamativos como comportamientos inteligentes que permiten una experiencia desafiante y dinámica. Cada enemigo fue modelado en 3D o descargado desde plataformas como Sketchfab y BlenderKit, adaptado posteriormente a Unity para integrarse al entorno de realidad mixta. A nivel funcional, los enemigos utilizan el sistema NavMesh de Unity, lo que les permite desplazarse por el espacio mapeado de forma autónoma, identificando rutas y evitando obstáculos del entorno real.

Modelado y animación

Los enemigos fueron seleccionados desde Sketchfab (modelos con licencia libre) y BlenderKit, importándolos a Unity y optimizando sus materiales, el escenario principal del videojuego está ambientado en una tienda de dulces colorida y estilizada, modelada en 3D con un enfoque lowpoly. Esta escena, obtenida desde BlenderKit bajo licencia libre, fue elegida por su estética vibrante y atractiva, ideal para un juego con enemigos tipo galleta y pizza.

Uno de los enemigos destacados del juego es Boo, un personaje inspirado en la clásica figura del fantasma de la saga Super Mario Bros. Este modelo fue obtenido desde Sketchfab y se caracteriza por su diseño caricaturesco, con cuerpo esférico, grandes colmillos y una lengua sobresaliente que refuerza su aspecto juguetón y a la vez amenazante. En el videojuego, Boo representa un enemigo flotante que no utiliza animaciones complejas, sino que avanza de forma constante hacia el jugador. Sin embargo, su movimiento está acompañado de pausas breves y tambaleos, simulando un comportamiento errático propio de un ente fantasmal.

La galleta de jengibre es uno de los enemigos más característicos del videojuego, destacándose tanto por su apariencia simpática como por su comportamiento agresivo. El modelo fue extraído de Sketchfab y representa una figura clásica navideña con detalles sencillos pero expresivos: botones azules, cejas decorativas y una gran sonrisa. en el juego esta galleta actúa como un enemigo rápido y persistente. Está animada con transiciones de movimiento, carrera y ataque mediante el sistema Mixamo, lo que le permite acercarse velozmente al jugador y causar daño cuando entra en rango. Su aspecto colorido y su comportamiento activo lo convierten en un oponente que contrasta fuertemente con enemigos más lentos o etéreos, como los fantasmas. Además, al morir, reproduce un sonido característico que refuerza el feedback del combate y marca su eliminación de la escena.

Pizza Steve es otro de los enemigos animados presentes en el videojuego, con un diseño llamativo y personalidad única. Este modelo fue extraído de Sketchfab y representa una porción de pizza antropomórfica con gafas oscuras, brazos delgados y una actitud relajada, que contrasta visualmente con su rol dentro del juego. A pesar de su apariencia carismática, Pizza Steve es un enemigo ágil que cuenta con animaciones de caminar, correr y atacar, integradas mediante Mixamo.

Inteligencia Artificial

Se implementó el NavMesh de Unity para definir áreas navegables y permitir que los enemigos se desplazaran inteligentemente por el espacio disponible, evitando obstáculos y adaptándose al entorno físico del usuario. Esto implicó:

  • Bakeo del NavMesh en tiempo de ejecución adaptado al plano real detectado.
  • Agentes NavMesh que se orientan dinámicamente hacia el jugador.

5. Interfaz e Interacción con el Usuario

game manager

La interacción se diseñó exclusivamente para gestos reconocidos por HoloLens:

  • Menú inicial con botón flotante.
  • Canvas flotantes que aparecen al finalizar cada nivel indicando el progreso y ofreciendo la opción de continuar.
  • Feedback visual mediante efectos de impacto y sonido cuando un enemigo es alcanzado.

6. Pruebas y Despliegue

El juego fue probado en entornos reales con el HoloLens 1 para verificar:

  • Correcta detección de planos.
  • Funcionalidad del NavMesh en espacios físicos reales.
  • Comportamiento fluido de los enemigos en diferentes tamaños de sala.
  • Latencia mínima al disparar o al interactuar con UI.

al terminarlas de seleccionar  get feature  proedemos a importarlas  y abrir nuestro unity, descargamos nuestra plataforma universal de windows.

una vez realizado damos clic en switch plataform, instalamos el audio windows> package manager

xr plug-in management  seleccionamos open XR> microsoft hollolens

Ve a:
Window > XR > Holographic Remoting for Play Mode

  1.  (si no aparece, instala Mixed Reality OpenXR Plugin desde el Package Manager)
  2. Se abrirá la ventana de emulación.
  3. Cambia:
    • Play Mode: de None a Remote to Device
    • En Remote Machine, escribe la IP del HoloLens
  4. Haz clic en Connect.
    • Si todo va bien, el estado cambiará a ✅ “Connected” en verde.

▶️ ¡Ahora prueba!

  • Presiona Play en el editor de Unity.
  • El contenido de tu escena (como tu cubo) se verá en el HoloLens en tiempo real.
  • Puedes mover la cabeza y ver la escena tal como si estuviera desplegada.

para compilar en lo hololens

abrimos buil settings

 damos clic en add open scenes para agregar la escena y manejamos la configuracion que se muestra

 y le damos buil 

Esto nos pedirá que seleccionemos una carpeta para que se cree nuestro archivo sln, una ves realizado debemos esperar a que cargue 

Una vez que tengamos ya nuestra carpeta con el archivo sln, procedemos a ejecutarlo con nuestro visual studio 2019, demora la capacidad que tenga nuestro equipo

Una vez esté abierto procedemos a colocar release  x 86 o x 32 depende del dispositivo que va a utilizar  y máquina local

Luego de haber seleccionado máquina local, procedemos a ubicarnos sobre el explorador  de soluciones que la encontramos al costado derecho

Realizamos clic derecho sobre “nombre del proyecto (Universal Windows)” > propiedades

Como la conexión se está realizando con las HoloLens se coloca la ip de las HoloLens, y se le da aplicar  y aceptar

Una vez realizado se da en ejecutar y esto empezará a compilar  

la compilación será de tres fases y puede demorar de 5 a 10 minutos para compilar, una vez termine de compilar en nuestras hololens  se abrira  las hololens 

7. Conclusiones y Aprendizajes

Este proyecto nos permitió comprender en profundidad las posibilidades y limitaciones de la realidad mixta en HoloLens 1, así como el uso de inteligencia artificial espacial con NavMesh. La experiencia fue enriquecedora en cuanto a diseño 3D, integración de UI en MR, y navegación de agentes virtuales en entornos físicos reales.

8.Créditos

Autor: sara sofía lis moreno, david steven rojas,  Luis Mateo Méndez Pinzón,

Editor: Magister ingeniero Carlos Iván Pinzón Romero

Código: UCMV

Universidad: Universidad Central

9. Citas / Referencia

BlenderKit. (s.f.). Galería de activos 3D.  https://www.blenderkit.com/asset-gallery?query=category_subtree:landmark+candy+order:_score+availability:free

Sketchfab. (s.f.). Boo - Super Mario Bros. https://sketchfab.com/3d-models/boo-super-mario-bros-e0e42b39cc5a4de79469ecc2d1b33f78

Sketchfab. (s.f.). Galleta de jengibre. https://sketchfab.com/3d-models/galleta-de-gengibre-1d6f7e0c514844e99ad87ac48eb8de40

Sketchfab. (s.f.). Pizza Steve. https://sketchfab.com/3d-models/pizzasteve-3a21b648c47b43caa3b05f7eb9146bbc

Unity Technologies. (s.f.). Construcción de NavMesh. https://docs.unity3d.com/es/2019.4/Manual/nav-BuildingNavMesh.html

Microsoft. (s.f.). Uso del emulador de HoloLens.https://learn.microsoft.com/en-us/windows/mixed-reality/develop/advanced-concepts/using-the-hololens-emulator

Xataka. (2023). Microsoft abandona las HoloLens: Es como decirles a Meta y a Apple que invertir en realidad aumentada es un error. https://www.xataka.com/realidad-virtual-aumentada/microsoft-abandona-hololens-como-decirles-a-meta-a-apple-que-invertir-realidad-aumentada-error

Meta. (s.f.). Drop Dead: The Cabin. Recuperado de https://www.meta.com/es-es/experiences/drop-dead-the-cabin/4691479430874595

Unity. (2017, 10 de abril). Unity NavMesh Tutorial - Basics [Video]. YouTube. https://www.youtube.com/watch?v=CHV1ymlw-P8

Microsoft. (2016, 28 de marzo). Microsoft HoloLens Emulator Tutorial: Step-by-Step Guide and Demo [Video]. YouTube. https://www.youtube.com/watch?v=0ImaZ_Aqe3I

Maria Jose Blanco Castillo. (23 de mayo 2023).Productividad con HoloLens 2: Una mirada a la realidad mixta.https://niixer.com/index.php/2023/05/23/productividad-con-hololens-2-una-mirada-a-la-realidad-mixta/

Sebastian ovallos. (3 de marzo 2023).Unity 101 o cómo hacer videojuegos de forma fácil . https://niixer.com/index.php/2024/03/03/unity-desde-cero/

Sebastian alberto garcia. (21 de marzo 2023). Red Dead Redemption: Un Hito en el Desarrollo de Videojuegos y su Impacto en Mi Infancia. https://niixer.com/index.php/2025/03/21/red-dead-redemption-un-hito-en-el-desarrollo-de-videojuegos-y-su-impacto-en-mi-infancia/

luis mateo mendez pinzon. (12 de abril 2025.Renderizado realista de utilizando blender https://niixer.com/index.php/2025/04/12/renderizado-realista-de-utilizando-blender/


juan esteban lugo (8 de abril 2025. Animación de Avatares 3D: Una guía práctica con Ready Player Me, Mixamo y Blender.https://niixer.com/index.php/2025/04/08/animacion-de-avatares-3d-una-guia-practica-con-ready-player-me-mixamo-y-blender/