3DAnimaciónInteligencia ArtificialMixamoNiixerTexturizadoVideojuegos

Último Amanecer en Unity

Este proyecto consiste en el desarrollo de un juego en Unity donde el jugador deberá recolectar la mayor cantidad posible de monedas mientras es perseguido por hordas de zombis. La mecánica central combina acción, evasión y estrategia, ya que cada encuentro con los enemigos reducirá la vida del personaje, poniendo en riesgo su supervivencia.

Una de las innovaciones clave del juego es la integración del sistema FaceBuilder, que permite al jugador personalizar el rostro del protagonista usando su propia cara. Esto añade un nivel extra de inmersión, haciendo que la experiencia sea más personal y envolvente.

Los zombis están controlados por un sistema de inteligencia artificial que detecta y sigue al jugador de forma dinámica.Su comportamiento adaptativo y su constante acecho convierten cada partida en un desafío diferente, aumentando la tensión y la re jugabilidad del juego.

Aqui presentamos el paso a paso.

Paso a Paso

Proceso para dar textura a personaje

Paso 1

Cargamos modelo de facebuilder creado

Paso 2

Importamos un modelo con esqueleto desde Mixamo; luego, le quitamos la cabeza y colocamos la nuestra, tal como se muestra.

Modelo

Paso 3

Una vez creado el modelo con el que jugaremos en Unity, lo exportamos en formato FBX junto con una copia de sus texturas.

Exportar
Rutas

Paso 4

Vamos a Unity y creamos una scene

Creación de escena

Paso 5 terreno en unity

Añadimos el terreno en Unity

Añadir Terrero
Terreno

Paso 6

Vamos a la Unity Asset Store y descargaremos todos los modelos gratuitos que necesitemos. Como están vinculados a una misma cuenta, podremos acceder a ellos directamente desde Unity.

Asset Store

Paso 7 importar en unity

Importamos los recursos de la siguiente manera:

Impotar

En el apartado MyAssets de Unity veremos todos los agregados, los descargaremos e importaremos.

My assets

Aca veremos todo lo importado a Unity

Importado

Paso 8

Importamos un paquete de texturas para el terreno

Texturas

Paso 9

Vamos al menú de edición del terreno

Terreno

Paso 10

Una vez en el menú, arrastramos el material seleccionado al terreno elegido, quedando de la siguiente manera; así, nuestros personajes no caerán al vacío.

Paso 11

Una vez aquí, importaremos nuestro modelo a Unity.

Paso 12

Arrastramos el modelo y arrastramos la textura de la cabeza hacia el terreno

Paso 13

De la misma forma haremos uso de los assets importados a Unity para hacer la funcionalidad en tercera persona

Paso 14

Lo llevamos a nuestra scene y lo acomodamos como procede.

Paso 15

Una vez lo tenemos bien ubicado llevamos nuestro modelo al player Armature.

Paso 16

Escondemos el modelo rosado en el inspector, desactivamos el modelo para que solo nos quede nuestro modelo

Paso 17

Ahora pondremos las animaciones a nuestro modelo en Unity

Paso 18

Usaremos el siguiente paquete que trae modelos de un templo.

Paso 19

En los prefabs vamos a ver todos los objetos disponibles

Paso 20

Agregaremos algunos arboles con el siguiente paquete

Paso 21

Una vez importado tendremos estos modelos

Paso 22

Una vez elegimos los que deseemos tendremos el siguiente resultado

Paso 23

Para nuestro juego queremos que los enemigos se muevan solos, por lo que usaremos el  NavMeshSurface, de esta manera el reconoce el terreno por el cual se pueden mover los enemigos y de esta manera evitar bugs.

Paso 24

Pondremos las monedas que el Player va recolectar,

Paso 25

Las pondremos en la scene y revisamos que el collaider quede bien ajustado

Paso 26

Diseñaremos un contador de las monedas que el player atrape y de esta manera podamos ver las que hacen falta.

En el menú superior vamos a : GameObject > UI > Text – TextMeshPro.

De esta manera podemos ver en pantalla el contador de las monedas

Paso 27

Le ponemos la etiqueta Coin y es importante que tenga el Sphere Collider y el Is Triggr activado

Paso 28

Creamos el script que tendrá el contador de las monedas y lo asignamos al player armature

using UnityEngine;

using UnityEngine.UI;

using TMPro;

using UnityEngine.SceneManagement;

public class CoinCollector : MonoBehaviour

{

    public int totalCoins = 8;

    private int collectedCoins = 0;

    public TMP_Text coinText;

    public string nextLevelName;

    public GameObject panelCompletado; // <– Panel de nivel completado

    void Start()

    {

        UpdateUI();

        if (panelCompletado != null)

        {

            panelCompletado.SetActive(false); // Ocultar al inicio

        }

    }

    void OnTriggerEnter(Collider other)

    {

        if (other.CompareTag(“Coin”))

        {

            collectedCoins++;

            Destroy(other.gameObject);

            UpdateUI();

            if (collectedCoins >= totalCoins)

            {

                Debug.Log(“¡Nivel completado!”);

                if (panelCompletado != null)

                {

                    panelCompletado.SetActive(true); // Mostrar panel

                    Time.timeScale = 0f; // Pausar juego

                }

            }

        }

    }

    void UpdateUI()

    {

        if (coinText != null)

        {

            coinText.text = “Monedas: ” + collectedCoins + “/” + totalCoins;

        }

    }

    // Llama esto desde el botón “Continuar”

    public void ContinuarNivel()

    {

        Time.timeScale = 1f; // Reanudar tiempo

        if (!string.IsNullOrEmpty(nextLevelName))

        {

            SceneManager.LoadScene(nextLevelName);

        }

    }

}

Paso 29

Ahora vamos asignarle el panel y el contador de monedas.

Paso 30

Ahora vamos a crear la barra de salud del player, con este script.

using UnityEngine;

using UnityEngine.Events;

using UnityEngine.SceneManagement;

public class Salud : MonoBehaviour

{

    public float saludInicial;

    public float saludActual;

    public UnityEvent eventoMorir;

    public GameObject Derrota;

    void Start()

    {

        saludActual = saludInicial;

        Derrota.SetActive(false);

    }

    public void CausarDaño(float valor)

    {

        saludActual -= valor;

        if (saludActual <= 0)

        {

            print(“Muerto !!! -> ” + gameObject.name);

            eventoMorir.Invoke();

            Derrota.SetActive(true);

            Time.timeScale = 0f;

        }

    }

    public void Reintentar()

    {

        print(“Intentando reiniciar…”);

        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);

        Time.timeScale = 1f;

    }  

}

Paso 29. Creamos un objeto vacio y asignamos el script, pondremos el PlayerArmature y el canvas de la barra de vida que tambien tiene este script

using UnityEngine;

using UnityEngine.UI;

public class BarraDeVida : MonoBehaviour

{

    public Salud saludJugador;

    public Image barraPorcentaje;

    void Update()

    {

        if (saludJugador != null && barraPorcentaje != null)

        {

            float porcentaje = saludJugador.saludActual / saludJugador.saludInicial;

            barraPorcentaje.fillAmount = porcentaje;

            // Cambiar color según el porcentaje

            if (porcentaje > 0.6f)

                barraPorcentaje.color = Color.green;

            else if (porcentaje > 0.3f)

                barraPorcentaje.color = Color.yellow;

            else

                barraPorcentaje.color = Color.red;

        }

    }

}

Paso 31

Ahora crearemos una función que permitirá reintentar cuando el jugador pierda.

Paso 32

Después de crear el panel, generamos el texto que se mostrará.

Paso 33

Ahora en el inspector

Paso 34

Además, creamos un script que nos permitirá reiniciar y avanzar al siguiente nivel.

using UnityEngine;

using UnityEngine.SceneManagement;

using Cinemachine;

public class GameManagerScript : MonoBehaviour

{

    public GameObject[] personajes;             

    public Transform puntoSpawn;               

    public CinemachineVirtualCamera camaraVirtual;

    public GameOverUI gameOverUI;               

    void Start()

    {

        int indexSeleccionado = PlayerPrefs.GetInt(“PersonajeSeleccionado”, 0);

        if (indexSeleccionado >= 0 && indexSeleccionado < personajes.Length)

        {

            GameObject personajeInstanciado = Instantiate(personajes[indexSeleccionado], puntoSpawn.position, Quaternion.identity);

            if (camaraVirtual != null)

            {

                camaraVirtual.Follow = personajeInstanciado.transform;

                camaraVirtual.LookAt = personajeInstanciado.transform;

            }

            else

            {

                Debug.LogWarning(“No se ha asignado la cámara virtual en el inspector.”);

            }

        }

        else

        {

            Debug.LogError(“Índice de personaje fuera de rango.”);

        }

    }

    public void Reintentar()

    {

        Time.timeScale = 1f;

        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);

    }

    public void SiguienteNivel()

    {

        Debug.Log(“Cargando siguiente nivel…”);

        Time.timeScale = 1f;

        int siguienteIndex = SceneManager.GetActiveScene().buildIndex + 1;

        if (siguienteIndex < SceneManager.sceneCountInBuildSettings)

        {

            SceneManager.LoadScene(siguienteIndex);

        }

        else

        {

            Debug.Log(“No hay más niveles. Fin del juego.”);

            if (gameOverUI != null)

            {

                gameOverUI.MostrarPantallaFinal();

            }

        }

    }

}

Paso 35

Ahora vamos a configurar los botones

Paso 36

Ahora para delimitar y que nuestro personaje no caiga al vacío, creamos 3 cubos modificando sus dimensiones y los ubicamos de esta manera

Paso 37

Después de esto, desactivamos el objeto para que deje de ser visible.

Paso 38

Ahora vamos a crear los enemigos, por lo que importaremos desde Mixamo las animaciones y el modelo con el que vamos a trabajar.

Crearemos 3 Scripts para los distintos enemigos que tendremos.

Paso 39

Script para Enemigo base.

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;

    public void Awake()

    {

        StartCoroutine(CalcularDistancia());

    }

    private void Start()

    {

        if(autoseleccionarTarget)

            target = Personaje.singleton.transform;

    }

    private void LateUpdate()

    {

        checkestado();

    }

    private void checkestado()

    {

        switch (estado)

        {

            case Estados.idle:

                Estadoidle();

                break;

            case Estados.Seguir:

                transform.LookAt(target, Vector3.up);

                Estadoseguir();

                break;

            case Estados.atacar:

                Estadoatacar();

                break;

            case Estados.muerto:

                Estadomuerto();

                break;

            default:

                break;

        }

    }

    public void cambiarestado(Estados e)

    {

        switch (e)

        {

            case Estados.idle:

                break;

            case Estados.Seguir:

                break;

            case Estados.atacar:

                break;

            case Estados.muerto:

                vivo = false;

                break;

            default:

                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()

    {

    }

    IEnumerator CalcularDistancia()

    {

        while (vivo)

        {

            yield return new WaitForSeconds(0.2f);

            if (target != null)

            {

                distancia = Vector3.Distance(transform.position, target.position); 

            }

        }

    }

public enum Estados

{

    idle = 0,

    Seguir = 1,

    atacar = 2,

    muerto = 3

}

}

Y tambien necesitamos de este

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]

public class Enemigo0orco : Enemigo

{

    private UnityEngine.AI.NavMeshAgent agente;

    public Animator animaciones;

    public float daño = 3;

    private void Awake()

    {

        base.Awake();

        agente = GetComponent<UnityEngine.AI.NavMeshAgent>();

    }

    public override void Estadoidle()

    {

        base.Estadoidle();

        if(animaciones!=null) animaciones.SetFloat(“velocidad”,0);

        if(animaciones!=null) animaciones.SetBool(“atacando”,false);

        agente.SetDestination(transform.position);

    }

    public override void Estadoseguir()

    {

        base.Estadoseguir();

        if(animaciones!=null) animaciones.SetFloat(“velocidad”,1);

        if(animaciones!=null) animaciones.SetBool(“atacando”,false);

        agente.SetDestination(target.position);

    }

    public override void Estadoatacar()

    {

        base.Estadoatacar();

        if(animaciones!=null) animaciones.SetFloat(“velocidad”,0);

        if(animaciones!=null) animaciones.SetBool(“atacando”,true);

        agente.SetDestination(transform.position);

        transform.LookAt(target, Vector3.up);

    }

    public override void Estadomuerto()

    {

        base.Estadomuerto();

        if(animaciones!=null) animaciones.SetBool(“vivo”,false);

        agente.enabled = false;

    }

    [ContextMenu(“Matar”)]

    public void Matar()

        {

            cambiarestado(Estados.muerto);

        }

    public void Atacar()

    {

        Personaje.singleton.salud.CausarDaño(daño);

    }

}

Paso 40

Modificamos los datos seleccionados con la flecha

Paso 41

Enemigo patrullero Script

using UnityEngine;

using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]

public class Patrullaje : Enemigo

{

    private UnityEngine.AI.NavMeshAgent agente;

    public Animator animaciones;

    public Transform[] CheckPoints;

    private int indice;

    public float distanciaCheckpoints;

    private float distanciaCheckpoints2;

    public float daño = 3;

    private void Awake()

    {

        base.Awake();

        agente = GetComponent<UnityEngine.AI.NavMeshAgent>();

        distanciaCheckpoints2 = distanciaCheckpoints * distanciaCheckpoints;

    }

    public override void Estadoidle()

    {

        base.Estadoidle();

        if(animaciones!=null) animaciones.SetFloat(“velocidad”,1);

        if(animaciones!=null) animaciones.SetBool(“atacando”,false);

        agente.SetDestination(CheckPoints[indice].position);

        if((CheckPoints[indice].position – transform.position).sqrMagnitude < distanciaCheckpoints2)

        {

            indice  = (indice + 1) % CheckPoints.Length;

        }

    }

    public override void Estadoseguir()

    {

        base.Estadoseguir();

        if(animaciones!=null) animaciones.SetFloat(“velocidad”,1);

        if(animaciones!=null) animaciones.SetBool(“atacando”,false);

        agente.SetDestination(target.position);

    }

    public override void Estadoatacar()

    {

        base.Estadoatacar();

        if(animaciones!=null) animaciones.SetFloat(“velocidad”,0);

        if(animaciones!=null) animaciones.SetBool(“atacando”,true);

        agente.SetDestination(transform.position);

        transform.LookAt(target, Vector3.up);

    }

    public override void Estadomuerto()

    {

        base.Estadomuerto();

        if(animaciones!=null) animaciones.SetBool(“vivo”,false);

        agente.enabled = false;

    }

    [ContextMenu(“Matar”)]

    public void Matar()

        {

            cambiarestado(Estados.muerto);

        }

    public void Atacar()

    {

        Personaje.singleton.salud.CausarDaño(daño);

    }

}

Paso 42

Modificaremos los puntos por los cuales se moverá el enemigo. Los “checkpoints” serán objetos vacíos que ubicaremos en las posiciones por donde queremos que pase.

Paso 43

Crear enemigo perseguidor

Creamos el script

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.AI;

public class Perseguidor : MonoBehaviour

{

    public Transform objetivo;

    public NavMeshAgent agente;

    public Animator animador;

    public float distanciaAtaque = 2.0f;

    public float daño = 3f;

    private float tiempoEntreAtaques = 1.5f;

    private float tiempoProximoAtaque = 0f;

    void Start()

    {

        if (objetivo == null)

        {

            GameObject jugador = GameObject.FindGameObjectWithTag(“Player”);

            if (jugador != null)

            {

                objetivo = jugador.transform;

            }

        }

    }

    void Update()

    {

        if (objetivo == null) return;

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

        if (distancia > distanciaAtaque)

        {

            agente.SetDestination(objetivo.position);

            if (animador != null)

            {

                animador.SetFloat(“velocidad”, agente.velocity.magnitude);

                animador.SetBool(“atacando”, false);

            }

        }

        else

        {

            agente.SetDestination(transform.position); // se detiene

            transform.LookAt(objetivo);

            if (animador != null)

            {

                animador.SetFloat(“velocidad”, 0);

                animador.SetBool(“atacando”, true);

            }

            if (Time.time >= tiempoProximoAtaque)

            {

                Atacar();

                tiempoProximoAtaque = Time.time + tiempoEntreAtaques;

            }

        }

    }

    void Atacar()

    {

        // Aquí puedes acceder a la salud del jugador si tienes un sistema

        if (objetivo.TryGetComponent<Salud>(out Salud saludJugador))

        {

            saludJugador.CausarDaño(daño);

        }

        Debug.Log(“¡Atacando al jugador!”);

    }

}

Paso 44

Una vez creado, asignamos el script al enemigo y configuramos los objetos correspondientes en el inspector.

Es importante mencionar que se debe agregar el componente Nav Mesh Agent para que todo lo que se implemente funcione correctamente.

Paso 45

Se agregará música o sonidos tanto para los enemigos como para el juego en general.
Para el entorno del juego, creamos un objeto vacío al que le asignamos el sonido que hayamos descargado.

Para crear la selección de personajes

Creamos el script

using UnityEngine;

using UnityEngine.SceneManagement;

public class SelectorPersonajes : MonoBehaviour

{

    public GameObject[] personajesDisponibles;

    public Transform puntoSpawn;

    private int indiceSeleccionado = 0;

    void Start()

    {

        MostrarPersonaje(indiceSeleccionado);

    }

    void MostrarPersonaje(int indice)

    {

        // Destruye el personaje anterior si existe

        foreach (Transform hijo in puntoSpawn)

        {

            Destroy(hijo.gameObject);

        }

        // Instancia el nuevo personaje

        GameObject personaje = Instantiate(personajesDisponibles[indice], puntoSpawn.position, Quaternion.identity);

        personaje.transform.parent = puntoSpawn;

    }

    public void SiguientePersonaje()

    {

        indiceSeleccionado = (indiceSeleccionado + 1) % personajesDisponibles.Length;

        MostrarPersonaje(indiceSeleccionado);

    }

    public void PersonajeAnterior()

    {

        indiceSeleccionado = (indiceSeleccionado – 1 + personajesDisponibles.Length) % personajesDisponibles.Length;

        MostrarPersonaje(indiceSeleccionado);

    }

    public void ConfirmarSeleccion()

    {

        PlayerPrefs.SetInt(“personajeSeleccionado”, indiceSeleccionado);

        SceneManager.LoadScene(“Juego”);

// Cambia por el nombre de tu escena de juego

    }

}

Paso 46 

Sonido para enemigos, agregamos el componente del sonido preferido al enemigo y ajustamos volumen  

Paso 47  creación de escena en unity

 Inicio de juego, creamos una escena nueva  

Para crear la selección de personajes  

Creamos el script 

using UnityEngine; 

using UnityEngine.SceneManagement; 

public class SelectorPersonajes : MonoBehaviour 

    public GameObject[] personajesDisponibles; 

    public Transform puntoSpawn; 

    private int indiceSeleccionado = 0; 

    void Start() 

    { 

        MostrarPersonaje(indiceSeleccionado); 

    } 

    void MostrarPersonaje(int indice) 

    { 

        // Destruye el personaje anterior si existe 

        foreach (Transform hijo in puntoSpawn) 

        { 

            Destroy(hijo.gameObject); 

        } 

        // Instancia el nuevo personaje 

        GameObject personaje = Instantiate(personajesDisponibles[indice], puntoSpawn.position, Quaternion.identity); 

        personaje.transform.parent = puntoSpawn; 

    } 

    public void SiguientePersonaje() 

    { 

        indiceSeleccionado = (indiceSeleccionado + 1) % personajesDisponibles.Length; 

        MostrarPersonaje(indiceSeleccionado); 

    } 

    public void PersonajeAnterior() 

    { 

        indiceSeleccionado = (indiceSeleccionado – 1 + personajesDisponibles.Length) % personajesDisponibles.Length; 

        MostrarPersonaje(indiceSeleccionado); 

    } 

    public void ConfirmarSeleccion() 

    { 

        PlayerPrefs.SetInt(“personajeSeleccionado”, indiceSeleccionado); 

        SceneManager.LoadScene(“Juego”); // Cambia por el nombre de tu escena de juego 

    } 

Paso 48 

Una vez creado el script, creamos el canvas, importamos los players y asignamos el script, configurando así los botones.

El desarrollo de este juego en Unity representa una experiencia completa de diseño interactivo, combinando mecánicas de supervivencia, recolección de objetos y personalización avanzada del personaje mediante FaceBuilder. La implementación de inteligencia artificial para los enemigos agrega un nivel de realismo y dificultad que mantiene al jugador constantemente alerta.

Este proyecto no solo demuestra habilidades técnicas en programación y diseño 3D, sino que también destaca la importancia de la inmersión y la personalización en los videojuegos modernos. Al permitir que los jugadores vean su propio rostro en el protagonista, se refuerza el vínculo emocional con el juego y se potencia la experiencia interactiva.

Resultado Final juego en unity:

Créditos

Autor : Daniela Caterine Quintero Aguilera y Juan Camilo Uribe Franco

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

Código: UCCG1-9

Universidad: Universidad Central

Referecias

Adobe Systems Incorporated. (s.f.). Mixamo. https://www.mixamo.com/ 

IRulosso.2024. ¡Cómo Crear un Personaje en Unity 3D! (Fácil y Rápido). [Video]. YouTube. https://www.youtube.com/watch?v=FURegpjm9sY

LuisCanary. 2020, abril 25. Generar ejecutable de tu Videojuego con Unity/Sacar .exe o .apk para PC o Android/Compilar. [Video]. YouTube. https://www.youtube.com/watch

Morion Tutoriales (Morion VO) 2022, mayo 1. Personaje en tercera persona Unity [Video]. YouTube. https://www.youtube.com/watch?v=dltY_3nMZPo

Morion Tutoriales (Morion VO) 2023, septiembre 25. IA para enemigos en Unity - Cap 1 [Video]. YouTube. https://www.youtube.com/watch?v=pKMp_MKIamc&list=PLjCKKt9GhZuK7mSzeecIeANnjrt7YEqi8&index=1

Maty Dev. 2024, marzo 24. El MEJOR TUTORIAL de selector de personajes de UNITY [Video]. YouTube. https://www.youtube.com/watch?v=R9ywZcMdMoU

Pixabay. (s.f.). Efectos de sonido de bosque horror - búsqueda. https://pixabay.com/es/sound-effects/search/bosque-horror/

Unity Technologies. (s.f.). Unity Asset Store. https://assetstore.unity.com/