Niixer

Desarrollo de Personajes 3D y Sistema de IA para Videojuegos con Unity y Blender

Introducción

Una guía para el desarrollo de personajes 3D animados y la implementación de sistemas de inteligencia artificial para videojuegos. El proceso abarca desde la creación y personalización de personajes utilizando las plataformas Mixamo y Blender, hasta la programación de comportamientos de IA avanzados en Unity.

La guía detalla el flujo de trabajo completo que incluye la selección e importación de modelos 3D desde Mixamo, la personalización de personajes mediante el intercambio de cabezas utilizando Facebuilder en Blender, y la configuración de sistemas de animación complejos en Unity. Además, se profundiza en la implementación de un sistema de IA para enemigos zombie que utiliza el sistema de navegación NavMesh de Unity, permitiendo comportamientos realistas de persecución y ataque.

El documento cubre aspectos técnicos esenciales como la configuración del Animator Controller, la creación de sistemas de apuntado con IK (Inverse Kinematics), y la solución de problemas comunes relacionados con la física y navegación de personajes. Esta guía está dirigida a desarrolladores de videojuegos que buscan crear experiencias inmersivas con personajes inteligentes y sistemas de animación fluidos.

  • Se realiza selección de los personajes en la plataforma mixamo
mixamo
Selección de personaje

  • Se procede a importar los personajes en la plataforma de Blender
Blender - carga de personaje
Importar personaje

  • Se realiza la importación del facebuilder de los personajes
Blender - Modificación de personaje
Facebuilder

  • Se retira la cabeza de los personajes para adaptar la nueva
Blender - Actualización de personaje
Cambio de cabeza

Blender - Ajuste de personaje
Ajuste del cambio de cabeza

  • Se realiza adaptación de la nueva cabeza de los personajes en malla de blender
Blender - Ajuste de cabeza
Cambio de cabeza completo

  • Para el segundo personaje se realiza el mismo proceso, descargamos el personaje por la pagina de Mixamo

Blender - Personaje 2
Importación de personaje en blender

  • Retiramos la cabeza e importamos la cabeza nueva
Importación de cabeza
Se importa la nueva cabeza

  • Ajustamos la cabeza a la posición y el tamaño necesario
Ajuste de cabeza
Ajuste de cabeza

Configuración del Animator Controller

Animator controller
Animator Controller

1: Abrir el Animator

  • Logrará ver el Animator Controller en la ventana Animator
  • En la ventana de Unity, ve a Window > Animation > Animator
  • Selecciona tu personaje en la jerarquía

Animator apertura
Animator

Datos
Tipo de datos

2: Crear parámetros

  • En la pestaña “Parameters” (esquina superior izquierda), haz clic en el “+”
  • Selecciona “Bool” y nómbralo “IsAiming”
  • Haz clic nuevamente en “+” y selecciona “Trigger”, nómbralo “Shoot”

3: Configurar transiciones

  • Arrastra tu animación de idle/movimiento normal al Animator (normalmente ya está)
  • Arrastra tu animación de apuntado al Animator
  • Haz clic derecho en la animación normal > “Make Transition” > arrastra a la animación de apuntado
  • Selecciona la flecha de transición y en el inspector:
    • En “Conditions” añade “IsAiming” = true
    • Ajusta “Transition Duration” a ~0.1 para que sea suave

4: Activar IK Pass

  • En el Animator, ve a la pestaña “Layers
  • Si no existe, crea una nueva capa con el “+” y nómbrala “UpperBody”
  • Selecciona la capa y marca “IK Pass”

2. Crear el objeto AimTarget (personajes)

Objeto AimTarget
Personaje imagen

1: Crear el objeto vacío

  • Haz clic derecho en tu personaje en la jerarquía
  • Selecciona “Create Empty”
  • Renombra el nuevo objeto como “AimTarget”

2: Posicionar el objeto (personajes)

  • Selecciona “AimTarget”
  • En el inspector (Transform), colócalo a unos metros delante del personaje:
    • Position Z: 3-5 (dependiendo de tu escena)
    • Position Y: ~1.5 (altura del brazo)

3. Configurar la capa UpperBody

1: Añadir animación a la capa

  • En el Animator, selecciona la capa “UpperBody”
  • Arrastra tu animación de apuntado/disparo a esta capa

2: Configurar peso

  • En la pestaña Layers, verás un slider “Weight” para la capa UpperBody
  • Pon este valor a 1 (para que se tenga prioridad sobre las animaciones base)

3: Ajustar máscara (opcional – Personajes)

  • Si solo quieres que afecte a la parte superior del cuerpo:
    • Haz clic en el engranaje de la capa
    • En “Avatar Mask”, crea o selecciona una máscara que solo incluya torso y brazos

Ajuste de mascara
Escena

Configuración del Canvas para los movimientos del jugador.

Se selecciona el player, se pone la vista 2D y comenzamos con la configuración y los plugins.

Configuraciones
Verificación escena

Posiciones
Posiciones
Información juego
Datos Animator

Se crea un espacio vacio para agregar una nueva animación de arma

Hornear la NavMesh Usando el Componente NavMeshSurface

  • ¿Qué es? El NavMeshSurface es el componente que se encarga de definir qué geometría debe ser usada para la NavMesh y de realizar el proceso de “baking”.
  • Instrucciones:
  1. Crea un Objeto Vacío para la NavMesh (Recomendado):
    • En la ventana “Hierarchy” (Jerarquía), haz clic derecho en un espacio vacío.
    • Selecciona Create Empty (Crear Vacío).
    • Renombra este nuevo GameObject (objeto de juego) a algo como “NavMeshBaker” o “LevelNavMesh”. Esto es útil para organizar tu escena.
  1. Agrega el Componente NavMeshSurface:
    • Con tu nuevo objeto “NavMeshBaker” seleccionado en la “Hierarchy”.
    • Ve a la ventana “Inspector” (normalmente a la derecha).
    • Haz clic en el botón Añadir Componente.
    • En la barra de búsqueda, escribe “NavMeshSurface”.
    • Selecciónalo para agregarlo a tu objeto.
  1. Configura y Hornea la NavMesh:
    • Una vez que el componente NavMeshSurface esté en tu objeto “NavMeshBaker”, verás sus propiedades en el Inspector.

Hornea la NavMesh
Escena ampliada

Si todo está configurado correctamente, verás la capa azul de la NavMesh superpuesta en las áreas transitables de tu escenario en la vista “Scene”.

Escribir el Código del ZombieIA (personaje)

  • ¿Qué es? Aquí es donde programaremos al zombie para que busque al jugador, lo persiga y lo ataque.

Instrucciones:

  1. Haz doble clic en el script “ZombieAI” en tu ventana “Project”. Esto abrirá Visual Studio (o el editor de código que tengas configurado con Unity).

Codigo:

using UnityEngine;

using UnityEngine.AI; // Necesitamos esto para usar NavMeshAgent

using- System.Collections; // Necesitamos esto para las corrutinas

public class ZombieAI : MonoBehaviour

{

    public- Transform playerTransform; // Variable para almacenar la referencia al jugador

    public float chaseRange = 10f; // Distancia a la que el zombie empezará a perseguir

    public float attackRange = 1.5f; // Distancia a la que el zombie empezará a atacar

    public- float attackCooldown = 2f; // Tiempo entre ataques

    public int attackDamage = 10; // Daño que inflige el zombie por ataque

    private NavMeshAgent agent; // Referencia al componente NavMeshAgent

    private bool isAttacking = false; // Bandera para controlar si el zombie ya está atacando

    void Start()

    {

        // Obtener la referencia al componente NavMeshAgent en este GameObject

        agent = GetComponent<NavMeshAgent>();

        // Intentar encontrar al jugador por su Tag, esto es crucial para que el zombie sepa a quién seguir.

        GameObject playerObject = GameObject.FindWithTag(“Player”);

        if (playerObject != null)

        {

            playerTransform = playerObject.transform;

        }

        else

        {

            Debug.LogError(“¡No se encontró el objeto con el Tag ‘Player’! Asegúrate de que tu jugador tiene el tag ‘Player’.”, this);

            enabled = false; // Desactiva el script si no se encuentra al jugador

        }

    }

    void Update()

    {

        if (playerTransform == null) return; // Si no hay jugador, no hacer nada

        // Calcular la distancia entre el zombie y el jugador

        float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);

        // Si el jugador está dentro del rango de persecución

        if (distanceToPlayer <= chaseRange)

        {

            // Mover el agente hacia la posición del jugador

            // Unity NavMeshAgent se encargará de la ruta inteligente

            agent.SetDestination(playerTransform.position);

            // Si el jugador está dentro del rango de ataque y no estamos atacando

            if (distanceToPlayer <= attackRange && !isAttacking)

            {

                // Detener el movimiento del agente para atacar

                agent.isStopped = true;

                StartCoroutine(AttackRoutine()); // Iniciar la rutina de ataque

            }

            else if (distanceToPlayer > attackRange && agent.isStopped)

            {

                // Si el jugador se alejó del rango de ataque, reanudar la persecución

                agent.isStopped = false;

            }

        }

        else

        {

            // Si el jugador está fuera del rango de persecución, detener el movimiento

            // Opcional: Podrías hacer que el zombie patrulle o esté ocioso aquí

            if (agent.hasPath) // Solo si tiene una ruta activa

            {

                agent.isStopped = true;

            }

        }

    IEnumerator AttackRoutine()

    {

        isAttacking = true; // Establecer bandera de ataque

        Debug.Log(“Zombie atacando!”); // Mensaje de depuración

        // TODO: Aquí pondrías la lógica de aplicar daño al jugador.

        // Por ahora, solo lo imprimimos. Necesitarías un script de “Health” en el jugador.

        //Ejemplo: PlayerHealth playerHealth = playerTransform.GetComponent<PlayerHealth>();

        // if (playerHealth != null) { playerHealth.TakeDamage(attackDamage); }

        // Esperar el tiempo de cooldown antes de poder atacar de nuevo

        yield return new WaitForSeconds(attackCooldown);

        isAttacking = false; // Resetear bandera de ataque

        // Reanudar el movimiento del agente si el jugador sigue en rango y no se ha movido

        if (Vector3.Distance(transform.position, playerTransform.position) <= attackRange)

        {

            // Mantenerse detenido si aún está en rango de ataque

            agent.isStopped = true;

        }

        else

        {

            // Si el jugador se alejó, reanudar la persecución

            agent.isStopped = false;

        }

    }

    // Esta función dibuja las esferas de rango en el editor para que las veas

    void OnDrawGizmosSelected()

    {

        // Rango de persecución (amarillo)

        Gizmos.color = Color.yellow;

        Gizmos.DrawWireSphere(transform.position, chaseRange);

        // Rango de ataque (rojo)

        Gizmos.color = Color.red;

        Gizmos.DrawWireSphere(transform.position, attackRange);

    }

}

Esto es un problema común y tiene varias causas posibles, especialmente cuando se trabaja con NavMeshAgent y la configuración inicial de un personaje.

Que el zombie “desaparezca” (generalmente significa que cae infinitamente o se teletransporta a un lugar inesperado) casi siempre está relacionado con cómo se está manejando su física o su posición inicial.

Vamos a depurar esto paso a paso.

Diagnóstico del Problema: Zombie Desaparece

Esto suele ocurrir por una de las siguientes razones:

  1. Cae por el escenario: El zombie no tiene un Collider o Rigidbody configurado correctamente, o su posición inicial está por debajo de la NavMesh o de cualquier superficie.
  1. Se teletransporta al origen (0,0,0) o a otro lugar: Esto puede pasar si el NavMeshAgent no encuentra una NavMesh válida para moverse o si hay un problema con su Base Offset.
  1. El modelo es tan pequeño que parece desaparecer: Menos probable, pero a veces pasa si la escala es incorrecta.

Soluciones y Verificaciones Paso a Paso:

Verificación 1: Posición Inicial del Zombie (Personaje)

  • Problema: Si el zombie empieza ligeramente por debajo de la NavMesh o de un objeto con un Collider, puede caer al vacío.
  • Solución:
  1. En la vista “Scene” (Escena), selecciona tu zombie.
  2. Mira su posición en el “Inspector” (Transform > Position).
  3. Asegúrate de que su coordenada Y (altura) esté por encima de la superficie de la NavMesh. debe ser solo lo suficiente para que no se “incruste”. Puedes arrastrarlo un poco hacia arriba con la herramienta de movimiento (icono de flechas) para levantarlo visualmente.

Verificación 2: Componentes Físicos (Collider y Rigidbody)

Aunque el NavMeshAgent se encarga del movimiento, los colliders y rigidbodies son importantes para la interacción física y para que el agente se “apoye” en el suelo.

  • Problema: Un NavMeshAgent necesita un Collider en el mismo GameObject para funcionar correctamente. A veces, si se usa un Rigidbody sin las configuraciones adecuadas, puede interferir.

Solución:

1: Selecciona tu zombie en la “Hierarchy”.

2: En el “Inspector”, verifica si tiene un componente Collider (por ejemplo, Capsule Collider o Box Collider).

  • Si es un modelo 3D con animaciones, un Capsule Collider suele ser lo mejor para personajes.
  • Si no tiene ninguno, haz clic en “Add Component”, busca “Capsule Collider” (o “Box Collider” si el diseño del zombie es simple).
  • Ajusta el tamaño y la posición del collider para que cubra a tu zombie. El Capsule Collider tiene Center (centro), Radius (radio) y Height (altura).

3: Rigidbody (Cuerpo Rígido):

  • Si tu zombie tiene un Rigidbody, asegúrate de que la opción “Is Kinematic” esté marcada. Esto es crucial cuando se usa NavMeshAgent. El NavMeshAgent se encarga del movimiento, y si el Rigidbody no es cinemático, intentará moverlo también, lo que causa conflictos y caídas.
  • Si no tiene un Rigidbody y solo tiene un Collider, el NavMeshAgent puede funcionar sin Rigidbody para la navegación básica. Sin embargo, para colisiones con otros objetos móviles o balas, un Rigidbody (cinemático) es necesario.

Cuerpo
Actualización animator

Navegación
Navegación

Ve a la barra de menú superior de Unity: Window > AI > Navigation. Esto abrirá la ventana “Navigation”.

En la ventana “Navigation”, haz clic en la pestaña Agents.

Deberías ver una lista de “Agent Types”. Selecciona el que dice Humanoid (es el predeterminado y el que usaremos).

A la derecha de la ventana “Navigation”, verás la configuración de ese tipo de agente. Ajusta estos valores:

  • Radius: Este es el radio del personaje. Para tu zombie, un valor de 0.5 metros es un buen punto de partida (si tu zombie es más grande o más pequeño, ajústalo).
  • Height: La altura del personaje. Para tu zombie, un valor de 1.8 a 2.0 metros es bueno.
  • Step Height: ¡Este es clave para las cuestas! Es la altura máxima de un “escalón” que el agente puede subir. Para terrenos irregulares como “Flooded Ground”, un valor de 0.3 a 0.4 metros suele funcionar bien. Esto permite que el zombie suba pequeñas elevaciones sin problema.
  • Max Slope: El ángulo máximo de inclinación que el agente puede caminar. Un valor de 45 grados es estándar y bueno para la mayoría de las pendientes.
  • Drop Height: La altura máxima que el agente puede caer sin considerar un borde como obstáculo. Un valor de 0.5 a 1.0 es seguro.

Configurar el GameManager en el Inspector:

  • Selecciona tu GameObject GameManager en la Hierarchy.
  • En el Inspector, en el componente Wave Spawner, verás muchos campos nuevos. Necesitas arrastrar y configurar:
    • Zombie Prefabs (Array):
      • Cambia el Size a 3.
      • Arrastra tus tres Prefabs (Zombie1 (1), Zombie_Fast, FatZombie) desde la ventana Project a las ranuras Element 0, Element 1, Element 2.
    • Zombie Count Text: Arrastra tu texto de UI (ZombieCountText) desde el GameUICanvas en la Hierarchy.
    • Wave Message Text: Arrastra tu texto de UI (WaveMessageText) desde el GameUICanvas en la Hierarchy.
    • Ajusta los valores de las nuevas variables a tu gusto para equilibrar la dificultad:
      • Base Time Between Waves, Min Time Between Waves, Time Between Waves Decrease Per Wave
      • Base Zombie Count, Zombies Increase Per Wave
      • Base Spawn Delay, Min Spawn Delay, Spawn Delay Decrease Per Wave
      • Base Zombie Health, Health Increase Per Wave
      • Base Zombie Damage, Damage Increase Per Wave
      • Spawn Radius, Min Distance From Player, Max Height Difference
      • Message Display Time, Message Move Time

Inspector

Una captura de pantalla de una computadora

El contenido generado por IA puede ser incorrecto.
Configuración inspector

Sistema de Disparo del Jugador (PlayerShooting.cs)

Este script irá adjunto a tu jugador o a tu cámara principal.

A. Crea el Script:

  • En la ventana Project, ve a tu carpeta Scripts.
  • Haz clic derecho > Create > cripting > empty C# Script y nómbralo PlayerShooting.
  • Abre el script y pega el siguiente código:

using UnityEngine;
public class PlayerShooting : MonoBehaviour

{

public float damagePerShot = 20f; // Cuánto daño hace cada disparo

public- float fireRate = 0.5f; // Tiempo entre disparos (0.5 significa 2 disparos por segundo)

public float weaponRange = 100f;  // Alcance del arma (raycast)

public LayerMask shootableLayer; // Define qué capas pueden ser impactadas por el disparo (¡Configurar en Inspector!)

private Camera playerCamera;

private float nextFireTime; // Para controlar el fire rate

void Start()

{

     playerCamera = GetComponentInChildren<Camera>(); // Busca la cámara como hijo (si la tienes como hijo del player)

     if (playerCamera == null)

     {

            Debug.LogError(“PlayerShooting: No se encontró una cámara en el jugador o sus hijos. Asegúrate de que el script está en el objeto correcto o que la cámara es hija.”);

         enabled = false; // Desactiva el script si no hay cámara

     }

}

void Update()

{

     // Disparar con el click izquierdo del ratón y si ha pasado suficiente tiempo desde el último disparo

     if (Input.GetMouseButtonDown(0) && Time.time >= nextFireTime)

     {

         Shoot();

            nextFireTime = Time.time + fireRate; // Establecer el tiempo del próximo disparo

     }

}

void Shoot()

{

     RaycastHit hit; // Almacena la información de lo que golpeó el raycast

     Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); // Rayo desde el centro de la pantalla

     // Realiza el raycast

     if (Physics.Raycast(ray, out hit, weaponRange, shootableLayer))

     {

            Debug.Log(“Golpeó: ” + hit.collider.name);

         // Intentar obtener el componente ZombieAI del objeto golpeado

         ZombieAI zombie = hit.collider.GetComponent<ZombieAI>();

         if (zombie != null)

         {

                zombie.TakeDamage(damagePerShot); // Llama al método TakeDamage del zombie

         }

         // Opcional: Si tienes efectos de disparo o impacto, actívalos aquí

         // Instantiate(hitEffectPrefab, hit.point, Quaternion.LookRotation(hit.normal));

     }

     else

     {

            Debug.Log(“No golpeó nada o solo golpeó capas no disparables.”);

     }

}

B. Configuración en Unity:

  1. Adjunta PlayerShooting.cs: Arrastra este script a tu GameObject Player (o el que contenga tu cámara principal).
  1. Configura la LayerMask:
    • En el Inspector del PlayerShooting script, verás un campo Shootable Layer.
    • Haz clic en el menú desplegable y selecciona Add Layer….
    • Crea una nueva Layer (Capa) llamada “Enemy” (o “Zombie”).
    • Vuelve al PlayerShooting script en el Inspector y ahora selecciona la capa “Enemy” en Shootable Layer.
  1. Asigna la Capa “Enemy” a tus Prefabs de Zombies:
    • Selecciona cada uno de tus Prefabs de zombies (Zombie1 (1), Zombie_Fast, FatZombie) en la ventana Project.
    • En el Inspector de cada Prefab, arriba de su nombre, verás un menú desplegable Layer.
    • Selecciona la capa “Enemy” que acabas de crear. ¡Esto es crucial para que el raycast los detecte!
  1. Ajusta el Damage Per Shot, Fire Rate y Weapon Range en el Inspector del PlayerShooting según tu gusto.

2. Indicador de Vida para Zombies (ZombieAI.cs y UI)

Cada zombie tendrá su propia barra de vida flotante.

A. Modificaciones en el Prefab de Zombie (UI):

  1. Abre uno de tus Prefabs de Zombie en modo de edición (haz doble clic en el Prefab en la ventana Project)
  1. Crea un Canvas para la barra de vida:
    • En la Hierarchy del Prefab, haz clic derecho > UI > Canvas.
    • Selecciona el nuevo Canvas. En su Inspector, cambia Render Mode a World Space.
Captura de pantalla de computadora

El contenido generado por IA puede ser incorrecto.
Zombie – Prefab
  • Ajusta el Rect Transform del Canvas:
    • Pos Y: Un valor positivo para que esté sobre la cabeza del zombie (ej. 2).
    • Width: 100 (o 1000, depende de tu escala, tendrás que ajustar).
    • Height: 10 (o 100).
    • Scale: Ajusta X, Y, Z a un valor muy pequeño (ej. 0.01 o 0.005) para que la barra de vida no sea gigante en el mundo.
  • Quita el componente Canvas Scaler del Canvas (haz clic en los tres puntos y Remove Component).
  1. Crea la Barra de Vida (UI Image):
    • Dentro del Canvas que acabas de crear (en la Hierarchy del Prefab), haz clic derecho > UI > Image. Este será el fondo de la barra de vida.
    • Nómbralo HealthBarBackground.
    • Cambia su Color a un gris oscuro o negro.
    • Ajusta su Rect Transform para que ocupe todo el Canvas: Left: 0, Top: 0, Right: 0, Bottom: 0.
    • Dentro de HealthBarBackground, haz clic derecho > UI > Image. Este será el relleno de la barra de vida.
    • Nómbralo HealthBarFill.
    • Cambia su Color a verde.
    • En su Inspector, en Image (Script), cambia Image Type a Filled.
    • Cambia Fill Method a Horizontal.
    • Fill Origin a Left.
    • Ajusta su Rect Transform para que ocupe el mismo espacio que el fondo.
  1. Haz que la Barra de Vida Mire a la Cámara (Nuevo Script Billboard.cs):
    • Crea un nuevo script llamado Billboard.cs.
    • Pega el siguiente código

using UnityEngine;

public class Billboard : MonoBehaviour

{

public Camera mainCamera; // Asigna_cámara_principal

void Start()

{

if (mainCamera == null)

     {

         mainCamera = Camera.main;

     }

     if (mainCamera == null)

     {

         Debug.LogError(“Billboard: No_se_encontró_la cámara_principal. Asegúrate_de_que_tu_cámara_tenga_el_Tag_’MainCamera’.”);

         enabled = false;

     }

}

void LateUpdate() // LateUpdate asegura ejecutar después de movimiento de cámara

{

     if (mainCamera != null)

     {

         // Objeto mira cámara, solo en el eje Y (para que no se incline)

            transform.LookAt(transform.position + mainCamera.transform.rotation * Vector3.forward,

                             mainCamera.transform.rotation * Vector3.up);

     }

}

Scene – Zombie

Configuración Final en los Prefabs de Zombies:

  1. Selecciona cada uno de tus Prefabs de Zombies en la ventana Project.
  2. En el Inspector, en el componente Zombie AI (Script), arrastra el HealthBarFill (la imagen verde que creaste dentro del Canvas del zombie) a la ranura Health Bar Fill Image.
  3. En el Canvas de cada Prefab de Zombie, en el script Billboard.cs, arrastra tu Main Camera (el GameObject de tu cámara principal en la escena) a la ranura Main Camera. Asegúrate de que tu cámara principal tenga el tag MainCamera.

3. Sistema de Vida del Jugador (PlayerHealth.cs y UI)

A. Crea el Script PlayerHealth.cs:Crea un nuevo script llamado PlayerHealth.

using_UnityEngine;

using UnityEngine.UI; // Necesario para Slider

using TMPro; // Necesario para TextMeshPro (si lo usas para mostrar vida numérica)

using_UnityEngine.SceneManagement; // Necesario para reiniciar la escena

public class PlayerHealth : MonoBehaviour

{

    public float maxHealth = 100f;

    /public float currentHealth;

    public Slider healthBarSlider; // Referencia al Slider de la UI para la barra de vida

    // public TextMeshProUGUI healthText; // Opcional: para mostrar la vida como número

    public GameObject gameOverPanel; // Referencia al Panel de Game Over

    /public TextMeshProUGUI gameOverMessageText; // Mensaje de Game Over

    public Button restartButton; // Botón de reinicio

    private WaveSpawner waveSpawner; // Para notificar que el jugador murió y detener oleadas

    void Awake() // Usar Awake para asegurar que se inicialice antes que otros scripts dependientes

    {

        currentHealth = maxHealth;

        if (healthBarSlider != null)

        {

            healthBarSlider.maxValue = maxHealth;

            healthBarSlider.value = currentHealth;

        }

        // if (healthText != null) healthText.text = currentHealth.ToString();

        if (gameOverPanel != null)

        {

            gameOverPanel.SetActive(false); // Asegurarse de que el panel de Game Over esté oculto al inicio

        }

        // Encontrar el WaveSpawner para detener las oleadas al morir el jugador

        waveSpawner = FindObjectOfType<WaveSpawner>();

        if (waveSpawner == null)

        {

            Debug.LogWarning(“PlayerHealth: No se encontró WaveSpawner. Asegúrate de que esté en la escena si necesitas detener las oleadas al morir.”);

        }

        // Asignar la función al botón de reinicio

        if (restartButton != null)

        {

            restartButton.onClick.AddListener(RestartGame);

        }

    }

    public void TakeDamage(float amount)

    {

        currentHealth -= amount;

        currentHealth = Mathf.Max(currentHealth, 0); // Asegurarse de que la salud no baje de 0

        if (healthBarSlider != null)

        {

            healthBarSlider.value = currentHealth;

        }

        // if (healthText != null) healthText.text = currentHealth.ToString();

        Debug.Log($”Jugador recibió {amount} de daño. Salud restante: {currentHealth}”);

        if (currentHealth <= 0)

        {

            Die();

        }

    }

    public void Heal(float amount)

    {

        currentHealth += amount;

        currentHealth = Mathf.Min(currentHealth, maxHealth); // Asegurarse de que la salud no supere la máxima

        if (healthBarSlider != null)

        {

            healthBarSlider.value = currentHealth;

        }

        // if (healthText != null) healthText.text = currentHealth.ToString();

        Debug.Log($”Jugador curado {amount}. Salud actual: {currentHealth}”);

    }

    void Die()

    {

        Debug.Log(“¡El jugador ha muerto!”);

        // Aquí puedes detener el tiempo, deshabilitar el movimiento del jugador, etc.

        Time.timeScale = 0f; // Detener el tiempo del juego

        if (gameOverPanel != null)

        {

            gameOverPanel.SetActive(true);

            if (gameOverMessageText != null)

            {

                gameOverMessageText.text = “¡Has Perdido!”;

            }

        }

        // Opcional: Deshabilitar el script de movimiento y disparo del jugador

        PlayerShooting playerShooting = GetComponent<PlayerShooting>();

        if (playerShooting != null) playerShooting.enabled = false;

        // Si tienes un script de movimiento de jugador, desactívalo también.

        // Notificar al WaveSpawner que el juego ha terminado

        if (waveSpawner != null)

        {

            // Puedes añadir un método en WaveSpawner para manejar el fin de juego,

            // por ahora, simplemente detendrá el flujo de oleadas al no haber más jugador.

        }

    }

    public void RestartGame()

    {

        Time.timeScale = 1f; // Reanudar el tiempo del juego antes de reiniciar la escena

        SceneManager.LoadScene(SceneManager.GetActiveScene().name); // Recarga la escena actual

    }

}

Paso a Paso: Implementando el Disparo (M1911 de Nokobot)

Asunción importante: Estás utilizando el sistema de Input System (New) de Unity, ya que lo configuramos antes.

Fase 1: Configuración del Arma en la Jerarquía del Jugador

Primero, necesitamos que el arma esté visible y correctamente posicionada en la mano de tu personaje.

  1. Localiza tu Prefab de Personaje:
    • En la ventana Hierarchy, selecciona tu GameObject de personaje principal (probablemente Player o ThirdPersonController).
  1. Encuentra el Hueso de la Mano Correcto:
    • Expande la jerarquía de tu personaje en el Hierarchy hasta encontrar el hueso de la mano derecha (o izquierda, según prefieras que dispare). Esto suele estar en una ruta similar a:
      • Player > PlayerArmature > Root > Hips > Spine > Spine1 > Spine2 > RightShoulder > RightArm > RightForeArm > RightHand.
    • Selecciona el hueso RightHand (o LeftHand).
  1. Posiciona el Arma en la Mano:
    • En la ventana Project, busca tu prefab o modelo de la M1911 del asset de Nokobot.
    • Arrastra el prefab/modelo de la M1911 desde la ventana Project y suéltalo sobre el hueso RightHand que seleccionaste en la Hierarchy. Esto hará que la M1911 se convierta en un hijo de RightHand.
    • Con la M1911 seleccionada en la Hierarchy, usa las herramientas de Move (W), Rotate (E) y Scale (R) para posicionar la M1911 en la mano de tu personaje. Ajusta su posición (Position), rotación (Rotation) y tamaño (Scale) hasta que se vea natural en la mano.
      • Sugerencia: Puedes entrar en modo Play brevemente para ver cómo se ve con la animación de inactividad, y luego ajustar los valores en el Inspector. Los cambios en modo Play no se guardan, así que anota los valores y luego aplícalos fuera del modo Play.
  1. Crea un Punto de Disparo (FirePoint):
    • Dentro de la jerarquía de tu M1911, haz clic derecho en el GameObject M1911 y selecciona Create Empty. Llama a este nuevo GameObject FirePoint.
    • Mueve y posiciona este FirePoint exactamente en la punta del cañón de la M1911. Este será el punto desde donde saldrán tus rayos de disparo o balas. Es importante que esté correctamente posicionado.

Fase 2: Script de Disparo (WeaponController.cs)

Ahora, vamos a crear la lógica de disparo.

  1. Crea un Nuevo Script:
    • En tu carpeta Assets/Scripts (o donde guardes tus scripts), haz clic derecho Create > C# Script.
    • Llama al script WeaponController.
  1. Adjunta el Script a la M1911:
    • Arrastra el script WeaponController que acabas de crear desde la ventana Project y suéltalo sobre el GameObject M1911 en tu Hierarchy.

Abre el Script WeaponController.cs y Pega el Código:

Scene – Personaje

using UnityEngine;

using System.Collections; // Necesario para Coroutines

// Asegúrate de que este namespace coincida con el de tus Starter Assets si los tienes

namespace StarterAssets

{

    public class WeaponController : MonoBehaviour

    {

        [Header(“Weapon Settings”)]

        /public float Damage = 10f; // Daño que hace el arma

        public float Range = 100f; // Alcance máximo del disparo

        public float FireRate = 0.1f; // Cadencia de disparo (segundos entre disparos)

        [Header(“References”)]

        /public Camera PlayerCamera; // La cámara principal de tu jugador (generalmente CinemachineTarget o Main Camera)

        public Transform FirePoint; // El punto desde donde saldrá el rayo/bala (el GameObject vacío que creaste)

        /public GameObject MuzzleFlashEffect; // El GameObject del efecto de flash de la boca del cañón

        public Animator WeaponAnimator; // El Animator del arma (M1911)

        private StarterAssetsInputs _input; // Referencia a tu script de entrada

        private float _nextTimeToFire = 0f; // Controla la cadencia de disparo

        void Start()

        {

            // Busca el componente StarterAssetsInputs en los padres o en el mismo objeto del personaje.

            // Asume que StarterAssetsInputs está en el GameObject raíz del personaje.

            _input = GetComponentInParent<StarterAssetsInputs>(); 

            if (_input == null)

            {

                Debug.LogError(“WeaponController: StarterAssetsInputs script not found on parent!”);

            }

            if (PlayerCamera == null)

            {

                // Intentar encontrar la cámara principal si no está asignada en el Inspector

                PlayerCamera = Camera.main; 

                if (PlayerCamera == null)

                {

                    Debug.LogError(“WeaponController: PlayerCamera not assigned and MainCamera not found!”);

                }

            }

            if (FirePoint == null)

            {

                Debug.LogError(“WeaponController: FirePoint not assigned! Create an empty GameObject at the gun’s muzzle.”);

            }

            if (MuzzleFlashEffect != null)

            {

                MuzzleFlashEffect.SetActive(false); // Asegúrate de que el flash esté desactivado al inicio

            }

        }

        void Update()

        {

            // Si el jugador presiona el botón de disparo y ha pasado suficiente tiempo desde el último disparo

            if (_input.shoot && Time.time >= _nextTimeToFire)

            {

                _nextTimeToFire = Time.time + FireRate; // Calcula cuándo se podrá disparar de nuevo

                Shoot(); // Llama a la función de disparo

            }

        }

        void Shoot()

        {

            // — 1. Animación de Disparo —

            if (WeaponAnimator != null)

            {

                // Asegúrate de tener un Trigger en tu Animator llamado “Fire” (o el que uses para disparar)

                WeaponAnimator.SetTrigger(“Fire”); 

            }

            // — 2. Efecto de Muzzle Flash —

            if (MuzzleFlashEffect != null)

            {

                StartCoroutine(MuzzleFlashRoutine()); // Inicia una Coroutine para activar y desactivar el flash

            }

            // — 3. Detección de Impacto (Raycasting) —

            RaycastHit hit;

            /*Dispara un rayo desde la cámara o desde el FirePoint si quieres que sea más preciso

            // Para 3ra persona, a menudo se usa la cámara para simular la “mira” del jugador

            // Si quieres que el rayo salga del arma en su dirección, usa FirePoint.forward

            /* Opción 1: Raycast desde el centro de la pantalla (más común para ThirdPerson)

            // Esto simula que el jugador apunta con la mira

            Ray ray = PlayerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); 

            if (Physics.Raycast(ray, out hit, Range))

            {

                Debug.Log(“Hit: ” + hit.collider.name);

                // Aquí puedes añadir lógica para:

                /* – Instanciar un efecto de impacto (ej. un agujero de bala) en hit.point

                /* – Llamar a un método de daño en el objeto impactado (ej. hit.collider.GetComponent<Health>().TakeDamage(Damage);)

            }

            // Opción 2: Raycast desde el FirePoint del arma (más preciso para el arma, pero menos para la mira del jugador)

            /* Esto podría no coincidir con el centro de la pantalla si no tienes un sistema de mira muy preciso

             if (Physics.Raycast(FirePoint.position, FirePoint.forward, out hit, Range))

            {

                 Debug.Log(“Hit (from gun): ” + hit.collider.name);

             }

             Para visualizar el rayo en el editor */

            Debug.DrawRay(ray.origin, ray.direction * Range, Color.red, 1f); 

        }

        // Coroutine para el efecto de flash de la boca del cañón

        IEnumerator MuzzleFlashRoutine()

        {

            MuzzleFlashEffect.SetActive(true);

            yield return new WaitForSeconds(0.05f); // Duración del flash (ajusta este valor)

            MuzzleFlashEffect.SetActive(false);

        }

    }

4: Configuración del Action Map (Input System)

Ahora que el script está listo, necesitamos configurar el Input System para reconocer el botón de disparo.

  1. Abre tu PlayerInput Action Map:
    • En la ventana Project, busca tu asset de Input Actions. Suele llamarse StarterAssets.inputactions o PlayerInputActions.
    • Haz doble clic en él para abrir la ventana Input Actions
  1. Añade una Nueva Acción de Disparo:
    • En la columna de Action Maps, selecciona Player.
    • En la columna de Actions, haz clic en el botón + (el signo de más).
    • Nombra la nueva acción Shoot.
  1. Asigna un Botón a la Acción Shoot:
    • Con la acción Shoot seleccionada, en la columna Properties (la más a la derecha), haz clic en el botón + debajo de No Bindings.
    • Haz clic en Listen. Ahora, presiona el botón que quieres usar para disparar (por ejemplo, el botón izquierdo del mouse).
    • Unity debería detectar Mouse/leftButton. Si es correcto, haz clic en Listen de nuevo para dejar de escuchar.
  1. Guarda el Asset de Input Actions:
    • Haz clic en el botón Save Asset en la parte superior derecha de la ventana Input Actions.

              window input actions

              Fase 5: Conectar Elementos en el Inspector

              Finalmente, conecta todas las referencias en el Inspector de tu M1911.

              1. Selecciona tu GameObject M1911 en la Hierarchy.
              1. En su Inspector, busca el componente WeaponController.
              1. Arrastra y asigna los siguientes campos:
                • Player Camera: Arrastra tu Main Camera (o el Cinemachine Virtual Camera Target) desde la Hierarchy hasta este campo.
                • Fire Point: Arrastra el GameObject FirePoint que creaste dentro de la M1911 hasta este campo.
                • Muzzle Flash Effect: Expande la jerarquía de tu M1911 para encontrar el GameObject que contiene el efecto de flash (podría llamarse “MuzzleFlash”, “Flash”, etc.). Arrástralo a este campo.
                • Weapon Animator: Arrastra el GameObject M1911 (o el hijo que tenga el Animator si no es el padre) hasta este campo.

              Fase 6: Configuración del Animator del Arma

              1. Selecciona tu GameObject M1911 en la Hierarchy.
              1. Abre la ventana Animator (Window > Animation > Animator).
              1. Crea un Trigger para el disparo:
                • En la ventana Animator, ve a la pestaña Parameters (normalmente a la izquierda).
                • Haz clic en el botón + y selecciona Trigger.
                • Nómbralo Fire (debe coincidir exactamente con el nombre WeaponAnimator.SetTrigger(“Fire”); en el script).
              1. Configura la Transición a la Animación de Disparo:
                • Arrastra tu clip de animación de disparo (el que se llama Fire o similar) desde la ventana Project a la ventana Animator para crear un nuevo estado.
                • Haz clic derecho en tu estado Any State (o tu estado de animación base, como Idle) y selecciona Make Transition hacia tu nueva animación de disparo.
                • Haz clic en la flecha de transición. En el Inspector, en la sección Conditions, haz clic en + y selecciona tu Trigger Fire.
                • Asegúrate de que Has Exit Time esté desactivado en la transición para que el disparo se reproduzca instantáneamente.
                • Opcionalmente, puedes crear una transición de la animación Fire de vuelta al estado Idle cuando la animación de Fire termine (con Has Exit Time activado en esa transición).

              Parte 1: Crear la Mira (Crosshair) en la UI

              Primero, vamos a poner la imagen de la mira en el centro de tu pantalla.

              1. Crea un Canvas para la UI:
                • En la ventana Hierarchy, haz clic derecho.
                • Selecciona UI > Canvas.
                • Renombra el nuevo GameObject a UICrosshairCanvas.
              1. Configura el Canvas:
                • Selecciona UICrosshairCanvas en la Hierarchy.
                • En el Inspector, en el componente Canvas:
                  • Cambia Render Mode a Screen Space – Camera.
                  • Arrastra tu Main Camera (o la cámara a la que está siguiendo Cinemachine, si es diferente) al campo Render Camera.
                • En el componente Canvas Scaler:
                  • Cambia UI Scale Mode a Scale With Screen Size.
                  • Pon Reference Resolution a 1920 x 1080 (o la resolución base de tu juego).
                  • Pon Screen Match Mode a Match Width Or Height.
                  • Pon Match a 0.5 (para equilibrar ancho y alto).
              1. Añade la Imagen de la Mira (Crosshair Image):
                • Haz clic derecho en UICrosshairCanvas en la Hierarchy.
                • Selecciona UI > Image.
                • Renombra el nuevo GameObject a CrosshairImage.
              1. Configura la Imagen de la Mira:
                • Selecciona CrosshairImage en la Hierarchy.
                • En el Inspector, en el componente Rect Transform:
                  • Haz clic en el cuadro de anclas (el que parece un cuadrado con dos flechas) y selecciona el ancla del centro (la que parece una cruz). Mantén Alt presionado al hacer clic para también establecer la posición.
                  • Asegúrate de que Pos X, Pos Y, Left, Right, Top, Bottom estén todos en 0.
                  • Ajusta Width y Height a un tamaño pequeño, por ejemplo, 20 o 30.
                • En el componente Image:
                  • En el campo Source Image, arrastra la textura de tu mira. Si no tienes una, puedes usar un sprite blanco temporalmente (crea uno con Assets > Create > Sprites > Square, luego cambia su color en el Inspector).
                  • Asegúrate de que Color sea blanco (RGBA 255,255,255,255) por ahora.

              Parte 2: Preparar a los Zombies (Capa y Script de Salud)

              Necesitamos que los zombies sean reconocibles para nuestro raycast y que puedan recibir daño.

              1. Crea una Capa (Layer) para los Zombies:
                • Selecciona uno de tus GameObjects de zombie en la Hierarchy.
                • En el Inspector, en la parte superior, al lado de Layer, haz clic en el menú desplegable.
                • Selecciona Add Layer….
                • En el primer espacio vacío (por ejemplo, User Layer 8 o User Layer 9), escribe Zombie.
                • Vuelve a seleccionar tu GameObject de zombie en la Hierarchy.
                • Cambia su Layer al recién creado Zombie.
                • IMPORTANTE: Si te pregunta Change Children? (¿Cambiar hijos?), selecciona Yes, change children para que todas las partes del modelo de tu zombie estén en la misma capa.
                • Repite esto para todos los GameObjects raíz de tus zombies en la escena.
              1. Crea un Script de Salud para los Zombies (Health.cs):
                • En tu carpeta Scripts (o donde organices tus scripts), haz clic derecho.
                • Selecciona Create > C# Script.
                • Nómbralo Health.
                • Abre el script Health.cs y pega el siguiente código:

              Conclusiones

              Las herramientas utilizadas demuestran la potencia y versatilidad del ecosistema actual de desarrollo de videojuegos 3D. Mixamo se consolida como una plataforma fundamental para desarrolladores, ofreciendo una amplia biblioteca de personajes y animaciones de alta calidad que acelera significativamente el proceso de prototipado y desarrollo. Su capacidad de proporcionar modelos listos para usar elimina las barreras técnicas iniciales y permite a los desarrolladores concentrarse en la mecánica del juego.

              Blender, por su parte, confirma su posición como la herramienta de modelado 3D más completa y accesible del mercado. Su integración con Facebuilder y las capacidades de edición de mallas permiten un nivel de personalización que seria costoso y complejo de lograr desde cero. La naturaleza open-source de Blender lo convierte en una opción especialmente atractiva para desarrolladores independientes y estudios con presupuestos limitados.

              Unity demuestra ser mucho más que un motor de juegos; es una plataforma integral que proporciona herramientas sofisticadas como el sistema NavMesh, el Animator Controller y el sistema IK. Estas características permiten implementar comportamientos complejos de IA y sistemas de animación avanzados sin requerir programación desde cero. La documentación extensa y la comunidad activa de Unity facilitan la resolución de problemas técnicos complejos.

              La sinergia entre estas tres herramientas crea un pipeline de desarrollo robusto y eficiente. Mixamo proporciona los assets base, Blender permite la personalización y optimización, y Unity ofrece la plataforma para dar vida a estos elementos con lógica de juego sofisticada. Este enfoque demuestra que las herramientas modernas han democratizado el desarrollo de videojuegos, haciendo accesible la creación de experiencias 3D de calidad profesional para desarrolladores de todos los niveles.

              Referencias

              Autodesk Inc. (2023). Mixamo (Versión 1.0) [Plataforma de animación 3D]. https://www.mixamo.com/

              Blender Foundation. (2023). Blender (Versión 4.0) [Software de modelado 3D]. https://www.blender.org/

              Unity Technologies. (2023). Unity (Versión 2023.3 LTS) [Motor de videojuegos]. https://unity.com/

              Unity Technologies. (2023). Unity NavMesh and Pathfinding. En Unity User Manual. https://docs.unity3d.com/Manual/nav-NavigationSystem.html

              Unity Technologies. (2023). Unity Animation and Animators. En Unity User Manual.
              https://docs.unity3d.com/Manual/AnimationOverview.html

              Maty Dev. (2024, abril 18). Movimiento y animaciones en Unity 3D - Episodio 2 [Video]. YouTube. https://www.youtube.com/watch?v=FC4IkQsEPfo

              Maty Dev. (2024, abril 25). Exportar TU JUEGO En Unity Para ANDROID Correctamente [Video]. YouTube. https://www.youtube.com/watch?v=foTtNekN9LM

              Unity Technologies. (2024, septiembre 11). Starter Assets – ThirdPerson | Updates in new CharacterController package (versión 1.1.6) [Recurso de software]. Unity Asset Store. https://assetstore.unity.com/packages/essentials/starter-assets-thirdperson-updates-in-new-charactercontroller-pa-196526

              Autores: Diego Gaitán Camargo, Juan Camilo Velasco Vega

              Editor: Carlos Iván Pinzón Romero

              Código: UCMV-08

              Universidad: Universidad Central