NiixerUnity

“Susurros” un videojuego 3D desarrollado en Unity

Los videojuegos han dejado de ser únicamente una forma de entretenimiento para convertirse en poderosas herramientas educativas que permiten aprender de manera interactiva y divertida. Unity, una plataforma de creación de videojuegos ampliamente utilizada en todo el mundo, ofrece la oportunidad de desarrollar experiencias educativas que combinan diversión y aprendizaje de forma natural.

Este artículo relata el proceso de creación de un videojuego académico, desde las primeras ideas hasta el producto final, mostrando cómo es posible transformar conceptos educativos en una experiencia que motive y enganche a los estudiantes. A lo largo de este recorrido, descubriremos que crear un videojuego educativo no solo permite enseñar contenidos específicos, sino que también desarrolla habilidades de resolución de problemas, creatividad y trabajo en equipo.

DISEÑO DE MENÚS CON ESCENAS

La primera etapa del diseño del videojuego se centra en la creación e implementación de la parte visual, especialmente de los menús interactivos. Estos permiten al usuario navegar por el sistema y acceder a funciones esenciales como el ajuste de volumen y brillo, la visualización de la barra de vida, el inventario, y la interacción con diálogos de NPCs, entre otros elementos clave para la experiencia de juego.

Escena de Inicio

Script utilizado

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.EventSystems;

public class PanelClickToChangeScene : MonoBehaviour, IPointerClickHandler
{
public string sceneName; // Nombre de la escena

public void OnPointerClick(PointerEventData eventData)
{
SceneManager.LoadScene(sceneName);
}
}

Se agrega el script en el canvas escribiendo la escena que el sistema debe mostrar apenas se de click a la pantalla

Escena de Menú principal

Para el fondo del menú principal, se creó una animación en Blender utilizando un escenario previamente desarrollado: una casa inspirada en la película Monster House y en referencias tomadas de la aplicación Randomnáutica. La animación se realizó utilizando el Timeline de Blender, donde, a través de keyframes, se generó una secuencia de movimientos que aportan dinamismo y ambientación a la escena.

Una vez finalizada la animación se exportó en un formato compatible para que el motor Unity pudiera reconocer y reproducir correctamente el video dentro del menú principal.

Formato de vídeo para Unity

Luego, para integrar el video en Unity, se importó el archivo a una carpeta del proyecto con el fin de mantener una organización adecuada. Para utilizarlo como fondo dentro del Canvas y asegurar su reproducción al iniciar la escena, se añadió un componente “Raw Image” en la jerarquía. Posteriormente, en la carpeta donde se encontraba el video, se creó un Render Texture, el cual permite proyectar la animación dentro del elemento de interfaz y visualizarla correctamente en el menú principal.

Lo siguiente que se realizó, fue agregar tres botones los cuáles representarán y tendrán la función de Jugar, Opciones y Salir.

El jugador al darle click al botón Jugar y al botón de Salir el sistema lo guiará a otras escenas de acuerdo a la selección, esto se hace por medio de un script de cambio de escena:

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class SceneLoader : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
public void Jugar()
{
SceneManager.LoadScene("ESCOGERPERSONAJE");
}

public void Salir()
{
Debug.Log("Salir...");
Application.Quit();

}
}

Scripts en el inspector

Se implementa el script en un GameObject vacío que creamos para que tenga un control de los eventos que definamos através de los scripts, como lo son el brillo, la pantalla completa, el volumen y en este caso el evento de escenas. Cuando se da click al botón se vincula el canvas y en la parte derecha llamamos a la función del script

Para el botón de opciones se realizó el siguiente script:

using UnityEngine;
using UnityEngine.SceneManagement;

public class cambioescenas : MonoBehaviour
{
public void IrMenuprincipal()
{
SceneManager.LoadScene("MENUPRINCIPAL");
}
public void IrMenuOpciones()
{
SceneManager.LoadScene("MENUOPCIONES");
}
public void IrTutorialARLYN()
{
SceneManager.LoadScene("TUTORIALArlyn");
}
public void IrTutorialTATIANA()
{
SceneManager.LoadScene("TUTORIALTatiana");
}
public void IrMAPA1()
{
SceneManager.LoadScene("MAPA1ARLYN");
}
public void IrMAPA2()
{
SceneManager.LoadScene("MAPA2TATIANA");
}
}

En esta escena, también se agrego un audio que actúa de forma global con el fin de poder escucharlo en otras escenas y poder alterarlo en la escena de Menú opciones.

El script para ese audio sería:

using UnityEngine;
using UnityEngine.SceneManagement;

public class GlobalMusicManager : MonoBehaviour
{
private static GlobalMusicManager instance;

[Header("Music Source")]
[SerializeField] private AudioSource musicSource;

[Header("Scene where music pauses")]
[SerializeField] private string gameplaySceneName = "ESCOGERPERSONAJE";

private void Awake()
{
if (instance != null)
{
Destroy(gameObject);
return;
}

instance = this;
DontDestroyOnLoad(gameObject);

if (musicSource == null)
musicSource = GetComponent<AudioSource>();
}

private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}

private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}

private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (musicSource == null) return;

if (scene.name == gameplaySceneName)
musicSource.Stop();
else
musicSource.UnPause();
}
}

También se agrego un un brillo como forma global con el fin de poder cambiarlo y que se aplique en otras escenas.

El script para ese audio sería:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Collections;

public class BrilloGlobalManager : MonoBehaviour
{
public static BrilloGlobalManager instance;


private Image panelBrilloGlobal;

public float suavizadoVelocidad = 0.5f;
private Coroutine brilloCoroutine;

private void Awake()
{
if (instance != null)
{
Destroy(gameObject);
return;
}

instance = this;
DontDestroyOnLoad(gameObject);


SceneManager.sceneLoaded += OnSceneLoaded;
}

private void OnDestroy()
{

SceneManager.sceneLoaded -= OnSceneLoaded;
}

private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{

GameObject panelObj = GameObject.Find("PanelBrillo");
if (panelObj != null)
{
panelBrilloGlobal = panelObj.GetComponent<Image>();

float savedValue = PlayerPrefs.GetFloat("brillo", 0.5f);
ApplyBrightness(savedValue);
}
}

private void Start()
{

}

public void ApplyBrightness(float alpha)
{
if (panelBrilloGlobal != null)
{
Color c = panelBrilloGlobal.color;
c.a = alpha;
panelBrilloGlobal.color = c;

PlayerPrefs.SetFloat("brillo", alpha);
PlayerPrefs.Save();
}
}


IEnumerator SuavizarBrilloCoroutine(float targetAlpha)
{

yield return null;


Color final = panelBrilloGlobal.color;
final.a = targetAlpha;
panelBrilloGlobal.color = final;
PlayerPrefs.SetFloat("brillo", targetAlpha);
PlayerPrefs.Save();
}
}

Escena Menú de Opciones

Pantalla Completa

Para esta escena se añadió un Toggle, utilizado como elemento interactivo para activar o desactivar una opción en este caso si dejar el juego con pantalla completa o en modo ventana. Se crea un GameObject vacío que nos permitirá controlar los script de las escena, por lo tanto el script que nos permite modificar esa configuración de pantalla es el siguiente:

using UnityEngine;
using UnityEngine.UI;

public class Pantallacompleta : MonoBehaviour {

public Toggle toggle;

void Start()
{
if(Screen.fullScreen)
{
toggle.isOn = true;
}
else
{
toggle.isOn = false;
}
}

void Update()
{

}

public void ActivarPantallaCompleta(bool pantallaCompleta)
{
Screen.fullScreen = pantallaCompleta;
}

}

Volumen y Brillo

Para que el jugador pueda alterar lo que esl brillo y el volumen, se agregaron dos sliders en el canvas, cada uno se centra alrededor de la imagen de fondo y se vinculan los respectivos scripts que permiten al usuario alterar el slider de volumen teniendo en cuenta el audio global que anteriormente se definió.

En el caso del brillo se crea otro panel el cual mostrará el efecto del script “Brillo” y se vinculan los parámetros que se establecieron en el código como lo son:

  • Slider: Sería el slider que agregamos al canvas donde se alterará el brillo.
  • Slider Value: guardará el valor del slider de acuerdo a la modificación del jugador.
  • Panel Brillo: Se vincula el panel que anteriormente creamos para ver los cambios del brillo al momento de cambiar el valor del slider del brillo.
  • Suavizado Velocidad: Permite que el cambio de brillo no sea brusco para el jugador.

Script para cambia el volumen

using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.UI;
public class volumen : MonoBehaviour
{
[SerializeField] private AudioMixer audioMixer;
public void CambiarVolumen(float volumen)
{
audioMixer.SetFloat("Volumen",volumen);
}
}

Script para cambiar el brillo con el slider

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class brillo : MonoBehaviour
{
public Slider slider;
public float sliderValue;
public Image panelBrillo;

public float suavizadoVelocidad = 0.5f;
private Coroutine brilloCoroutine;

void Start()
{

sliderValue = PlayerPrefs.GetFloat("brillo", 0.5f);
slider.value = sliderValue;


panelBrillo.color = new Color(panelBrillo.color.r, panelBrillo.color.g, panelBrillo.color.b, sliderValue);
}

public void ChangeSlider(float valor)
{

sliderValue = valor;
panelBrillo.color = new Color(panelBrillo.color.r, panelBrillo.color.g, panelBrillo.color.b, valor);


PlayerPrefs.SetFloat("brillo", sliderValue);


}

public void IniciarSuavizado()
{
if (brilloCoroutine != null)
{
StopCoroutine(brilloCoroutine);
}
brilloCoroutine = StartCoroutine(SuavizarBrillo(sliderValue));
}

IEnumerator SuavizarBrillo(float targetAlpha)
{
float currentAlpha = panelBrillo.color.a;
float time = 0f;

while (time < 1f)
{
time += Time.deltaTime / suavizadoVelocidad;
float newAlpha = Mathf.Lerp(currentAlpha, targetAlpha, time);


panelBrillo.color = new Color(
panelBrillo.color.r,
panelBrillo.color.g,
panelBrillo.color.b,
newAlpha
);
yield return null;
}

panelBrillo.color = new Color(panelBrillo.color.r, panelBrillo.color.g, panelBrillo.color.b, targetAlpha);
}
}

Escena de selección de personajes

Para la escena de Selección de personajes, el primer paso que se realizó fué incorporar un Canvas dentro del proyecto. Una vez creado el Canvas, se añadió un Panel cuya función principal es servir como contenedor visual. Sobre este panel se integró una imagen de fondo, lo que contribuye a mantener coherencia estética y fluidez en la experiencia visual entre las distintas escenas del videojuego.

Posteriormente, se incluyeron dos componentes “Image (UI Image)”, los cuales forman parte de la interfaz gráfica. Estos permiten mostrar elementos adicionales como ilustraciones, marcos o decoraciones relacionadas con la selección de personaje. Su correcta disposición y jerarquía dentro del Canvas garantizan una interfaz intuitiva y agradable para el jugador.

Las imágenes de cada personaje fueron creadas en Krita, tomando como base los modelos 3D generados en Avaturn. Para que el jugador pudiera elegir un personaje y cargarlo en la siguiente escena, se añadieron dos botones debajo de cada ilustración. A estos botones se les ajustó el color del estado “Highlighted” para indicar visualmente cuando el cursor está sobre ellos.

Además, se creó un script llamado “CambioDeEscenas”, encargado de registrar la elección del jugador y cargar la escena correspondiente con el personaje seleccionado.

Este script se llama en el inspector de cada botón en la parte de cuando ocurre la acción de “Click” se active el script y así el sistema guía al usuario de acuerdo a la escena especificada. Para mayor facilidad, creamos dos mapas de tutorial, uno para el personaje “Arlyn” y otro para el personaje “Tatiana”.

Imagen de la escena”Escoger Personaje” en desarrollo en el motor Unity

Escena Mapa 1 “Tutorial”

Para esta escena, se configura que al iniciar el nivel, el jugador está limitado en su movimiento, ya que el tutorial por medio texto le explicará que tecla debe oprimir para ir completando las solicitudes como:

  • W para moverse hacia delante.
  • S para moverse hacia atrás.
  • A para moverse hacia la izquierda.
  • D para moverse hacia la derecha.

Una vez completadas las anteriores solicitudes el jugador podrá moverse libremente por el mapa, sin tener errores de colliders o que tenga una caída infinita.

Script Tutorial

using StarterAssets;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;

public class TutorialJugador : MonoBehaviour
{
public TextMeshProUGUI textoInstrucciones;
public AudioSource audioTutorial;

public GameObject panelTutorial;

private ThirdPersonController thirdPersonController;
private PlayerInput playerInput;
private StarterAssetsInputs starterInputs;

private int pasoActual = 0;
private float tiempoPresionado = 0f;
private float tiempoRequerido = 0.5f;

// Para Android (botones)
private bool upPressed, downPressed, leftPressed, rightPressed;

void Start()
{
thirdPersonController = FindFirstObjectByType<ThirdPersonController>();
playerInput = thirdPersonController.GetComponent<PlayerInput>();
starterInputs = thirdPersonController.GetComponent<StarterAssetsInputs>();

// Bloquear movimiento normal
playerInput.actions["Move"].Disable();
playerInput.actions["Look"].Disable();

MostrarPaso(0);

if (audioTutorial != null)
audioTutorial.Play();
}

void Update()
{
Vector2 input = Vector2.zero;

// -- PC: teclas WASD --
bool pcUp = Keyboard.current != null && Keyboard.current.wKey.isPressed;
bool pcDown = Keyboard.current != null && Keyboard.current.sKey.isPressed;
bool pcLeft = Keyboard.current != null && Keyboard.current.aKey.isPressed;
bool pcRight = Keyboard.current != null && Keyboard.current.dKey.isPressed;

// -- ANDROID: Joystick Starter Assets --
Vector2 joystick = starterInputs.move;

switch (pasoActual)
{
case 0: // Avanzar (W / ↑ / joystick arriba)
if (pcUp || upPressed || joystick.y > 0.5f)
{
input = new Vector2(0, 1);
ContarTiempo();
}
break;

case 1: // Retroceder
if (pcDown || downPressed || joystick.y < -0.5f)
{
input = new Vector2(0, -1);
ContarTiempo();
}
break;

case 2: // Izquierda
if (pcLeft || leftPressed || joystick.x < -0.5f)
{
input = new Vector2(-1, 0);
ContarTiempo();
}
break;

case 3: // Derecha
if (pcRight || rightPressed || joystick.x > 0.5f)
{
input = new Vector2(1, 0);
ContarTiempo();
}
break;
}


if (pasoActual < 4)
{
thirdPersonController.useOverrideInput = true;
thirdPersonController.overrideMoveInput = input;
}
else
{

thirdPersonController.useOverrideInput = false;
playerInput.actions["Move"].Enable();
playerInput.actions["Look"].Enable();
}
}

void ContarTiempo()
{
tiempoPresionado += Time.deltaTime;

if (tiempoPresionado >= tiempoRequerido)
{
tiempoPresionado = 0;
SiguientePaso();
}
}

void MostrarPaso(int paso)
{
switch (paso)
{
case 0: textoInstrucciones.text = "Presiona W o toca ↑ para avanzar"; break;
case 1: textoInstrucciones.text = "Presiona S o toca ↓ para retroceder"; break;
case 2: textoInstrucciones.text = "Presiona A o toca ← para moverte a la izquierda"; break;
case 3: textoInstrucciones.text = "Presiona D o toca → para moverte a la derecha"; break;
}
}

void SiguientePaso()
{
pasoActual++;

if (pasoActual < 4)
{
MostrarPaso(pasoActual);
}
else
{
textoInstrucciones.text =
"¡Bien hecho! Ahora acércate a Suzanne e interactúa con ella con E o tocando la pantalla.";

StartCoroutine(CerrarTutorial());
}
}

IEnumerator CerrarTutorial()
{
yield return new WaitForSeconds(5f);

textoInstrucciones.text = "";
panelTutorial.SetActive(false);
}

// ----------- BOTONES PARA ANDROID -------------
public void PressUp(bool pressed) => upPressed = pressed;
public void PressDown(bool pressed) => downPressed = pressed;
public void PressLeft(bool pressed) => leftPressed = pressed;
public void PressRight(bool pressed) => rightPressed = pressed;
}

Este script toma en cuenta el script de ThirdPersonController con el fin de que el personaje pueda moverse con las teclas que normalmente conocemos que son W,S,A,D, que nos ofrece un Asset llamado “Starter Assets : ThirdPerson” que descargamos de la Asset Store que nos permite Unity que importamos para el proyecto.

Integración de un NPC

Lo siguiente en el tutorial es que el jugador pueda interactuar con Suzanne como guía, para activar ese evento el jugador debe acercarse a la monita para que se active el pane de interacción con texto incluido, el jugador con la tecla “E” puede seguir hablando con el NPC desbloqueando todos sus diálogos.

Script que permite al jugador interactuar con el NPC con la tecla “E” o tocando la pantalla en caso de android

using UnityEngine;
using TMPro;

public class NPCDialogue : MonoBehaviour
{
public GameObject dialoguePanel;
public TextMeshProUGUI dialogueText;

[TextArea(2, 4)]
public string[] dialogos;
int dialogoActual = 0;

bool playerInside;

void Start()
{
dialoguePanel.SetActive(false);
}

void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
playerInside = true;
dialoguePanel.SetActive(true);

dialogoActual = 0;
dialogueText.text = dialogos[dialogoActual];
}
}

void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
playerInside = false;
dialoguePanel.SetActive(false);
}
}

void Update()
{
if (!playerInside) return;

// ------- PC: Tecla E -------
if (Input.GetKeyDown(KeyCode.E))
{
CambiarDialogo();
}

// ------- ANDROID: tocar pantalla -------
if (Input.touchCount > 0)
{
Touch t = Input.GetTouch(0);

if (t.phase == TouchPhase.Began)
{
CambiarDialogo();
}
}
}

// Botón extra opcional para Android
public void CambiarDialogoBoton()
{
if (playerInside)
{
CambiarDialogo();
}
}

void CambiarDialogo()
{
dialogoActual++;

if (dialogoActual >= dialogos.Length)
{
dialogoActual = dialogos.Length - 1;
}

dialogueText.text = dialogos[dialogoActual];
}
}

El script permite generar una serie de diálogos guardados en una lista donde se pueden agregar la cantidad que se requiera y el orden en que se van a mostrar en cada interacción del jugador con el NPC de Suzanne. Además se agregó un “Audio Source” que al iniciar la escena suena lejos pero entre más nos acercamos al npc se escucha más fuerte.

Implementación de Tutorial

Usamos el software de modelado 3D “Blender” para el diseño del mapa del tutorial, cada pieza como la calle, las cercas, la estación, se realizaron a partir de primitivas, para esta escena se utilizaron modelos de SketchFab como lo son los faros de luz, el bus y los árboles.

Una vez definido el modelo en Blender, vamos a quitar componentes como cámara y luces, con tal de que al importar el mapa al motor de Unity no ocurre errores ni que la cámara se sobreponga con la principal. El modelo se exporta en formato FBX, teniendo en cuenta la cantidad de caras que tenga y la corrección de normales con tal que el jugador pueda visualizar bien los detalles del mapa sin errores ni bugs.

Ya importado el modelo en el motor, el siguiente paso es agregarlo a la escena y poder ajustar su tamaño a una escala de 1,1,1 esto con el fin de que el juego no consuma muchos recursos al equipo con el cual estamos desarrollando el videojuego. Agregamos los árboles y quitamos la luz default que nos ofrece Unity al momento de crear una escena, esto con el fin de que podamos nosotros mismos implementar nuestra porpias luces con el fin de tener el siguiente resultado:

Lo siguiente es agregar al escenario un mesh collider con el objetivo que el personaje a spawnear no sufra una caída infinita al vacío del mapa de Unity.

SONIDO 3D

Para la escena del tutorial, al bus se le agregó un “Audio Source” de un sonido de motor, pero se realizaron unas ciertas configuraciones con tal de que cuando se inicie la escena no se escuche el sonido del bus sino cuando el jugador se acerque al prefab del bus. La configuraciones fueron las siguientes:

  • Spatial Blend dejarlo en 3D
  • Activar el Play On Awake
  • Habilitar el Loop para que el sonido no se detenga nunca
  • Dejamos el volumen en 0.5 para que no se escuche tan fuerte al acercarse al bus

Unity ofrece un sistema de audio espacial 3D que permite simular cómo se comporta un sonido en un entorno tridimensional, dependiendo de la distancia entre la fuente sonora y el jugador.

Implementación de personajes a las escenas de los mapas

Para la implementación de un modelo a unity, descargarmos un asstes que nos da como base el esqueleto para que nuestro muñeco se puede ver.

Una vez importado nuestro modelo, buscamos el prefab del paquete Starter Assets, le damos click derecho y buscamos la sección “Prefab” y le damos a la opcion “Unpack Completely”, esto nos permitirá poder modificar el prefab sin alterar el original.

Una vez que cuadramos la posición y escala de nuestro modelo, en la parte de jerarquía desactivamos el “Geomtry” para que no aparezca el modelo default del asset.

Sistema de partículas para las luciernágas

Para recrear el efecto de luciérnagas en la escena, se añadió un Sistema de Partículas dentro de la jerarquía de la escena. Este sistema permite simular pequeños puntos de luz flotando y moviéndose de manera aleatoria.

Una vez incorporado el Particle System, se ajustaron parámetros como:

  • Max Particles: Define la cantidad máxima de partículas que pueden existiar al mismo tiempo.
  • Size over Lifetime: Permite que el tamaño de la partícula cambia a lo largo de su vida, simulando un parpadeo suave.
  • Shape: Controla la forma y el área desde donde las partículas son emitidas.
  • Velocity over Lifetime: Modifica la velocidad de las partículas a medida que avanzan en su ciclo de vida
  • Color over Lifetime: Permite que el color de cada partícula cambie durante su existencia.
  • Emission: Controla cuántas partículas se generan por segundo.
  • Movimiento Aleatorio: Agrega variación aleatoria en la dirección y velocidad del movimiento.
  • Play On Awake — Activado

Esto con el fin de obtener partículas que imitaran el comportamiento natural de las luciérnagas. Además, se habilitó un material con brillo suave para que los puntos emitieran una ligera luminosidad, reforzando el efecto visual.

Escena Minijuego “Preguntas de Cultura General”

Para la escena del minijuego, se diseño el mapa desde blender para tener mayor precisión de modelado y básicamente se decidió reutilizar el escenario del tutorial pero en este caso el bus de moverá a los largo de la carretera y el minijuego se ejecutará de fondo.

Para eso se importa el modelado teniendo en cuenta y cuidado con las normales para no tener errores de colliders negativos o que el bus no pueda mantener su animación a lo largo de la carretera.

Para el diseño del minijuego, se decidió implementar una mecánica de preguntas similar a ¿Quién quiere ser millonario?, para eso agregamos un canvas, lo siguiente es agregar un panel donde irá el fondo que en este caso será un celular simulando que el personaje lo está utilizando mientras está viajando.

Para las preguntas, se realizó un script donde mostrar las preguntas en el gameobject de Text Mesh que agregamos en el canvas, esto con el objetivo de actualizar las preguntas.

SCRIPT Utilizado para el minijuego

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;
using System.Collections;

public class MiniJuegoPreguntas : MonoBehaviour
{
[Header("UI")]
public GameObject panelCelular;
public TextMeshProUGUI txtPregunta;
public TextMeshProUGUI txtPuntaje;
public Button btnSi;
public Button btnNo;

[Header("Preguntas")]
[TextArea]
public string[] preguntas;
public bool[] respuestas;

[Header("Audio por pregunta")]
public AudioClip[] audiosPreguntas;
public AudioSource audioSource;

[Header("Sonidos de Respuesta")]
public AudioClip sonidoCorrecto;
public AudioClip sonidoIncorrecto;
public AudioSource audioFX;

private int preguntaActual = 0;
private int puntuacion = 0;

[Header("Configuración de Escenas")]
public string escenaGanar = "MAPA1ARLYN";
public string escenaPerder = "animacionbusarlyn";

void Start()
{
if (panelCelular != null)
panelCelular.SetActive(false);

StartCoroutine(MostrarPanelLuego());

btnSi.onClick.AddListener(() => Responder(true));
btnNo.onClick.AddListener(() => Responder(false));
}

IEnumerator MostrarPanelLuego()
{
yield return new WaitForSeconds(12f);

panelCelular.SetActive(true);
MostrarPregunta();
}

void MostrarPregunta()
{
if (preguntaActual < preguntas.Length)
{
txtPregunta.text = preguntas[preguntaActual];
txtPuntaje.text = "Puntaje: " + puntuacion + "/5";

Debug.Log("Mostrando pregunta #" + preguntaActual);

ReproducirAudioPregunta();
}
else
{
FinalizarJuego();
}
}

void ReproducirAudioPregunta()
{
if (audioSource != null && audiosPreguntas.Length > preguntaActual)
{
audioSource.clip = audiosPreguntas[preguntaActual];
audioSource.Play();
}
}

void Responder(bool respuestaJugador)
{
if (preguntaActual >= preguntas.Length)
return;

// RESPUESTA CORRECTA
if (respuestaJugador == respuestas[preguntaActual])
{
puntuacion++;
if (audioFX != null && sonidoCorrecto != null)
audioFX.PlayOneShot(sonidoCorrecto);
}
else
{
// RESPUESTA INCORRECTA
if (audioFX != null && sonidoIncorrecto != null)
audioFX.PlayOneShot(sonidoIncorrecto);
}

preguntaActual++;
MostrarPregunta();
}

void FinalizarJuego()
{
Debug.Log("Puntuación final = " + puntuacion);

if (puntuacion >= 5)
{
SceneManager.LoadScene(escenaGanar);
}
else
{
SceneManager.LoadScene(escenaPerder);
}
}
}

Escena Mapa 2 “Recolecta las monedas”

Script de inventario del jugador

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

public class PlayerInventory : MonoBehaviour
{
[Header("Monedas")]
public int monedasTotales = 0;
public int monedasObjetivo = 10;

[Header("UI")]
public TextMeshProUGUI txtMonedas;
public TextMeshProUGUI txtMensajeFinal;

[Header("UI Llaves (opcional)")]
public TextMeshProUGUI txtLlaves; // ← NUEVO: para ver las llaves en pantalla

[Header("Llaves")]
private List<string> llavesObtenidas = new List<string>();

void Start()
{
// Para probar: siempre iniciar con 10 monedas.
ReiniciarMonedas();

CargarLlaves();
ActualizarUI();
ActualizarUILlaves();

if (txtMensajeFinal != null)
txtMensajeFinal.gameObject.SetActive(false);
}

// ==================== MONEDAS ====================
void ReiniciarMonedas()
{
monedasTotales = 0;
PlayerPrefs.SetInt("MonedasTotales", monedasTotales);
PlayerPrefs.Save();
}

public void AgregarMonedas(int cantidad)
{
monedasTotales += cantidad;

PlayerPrefs.SetInt("MonedasTotales", monedasTotales);
PlayerPrefs.Save();

ActualizarUI();

if (monedasTotales >= monedasObjetivo && txtMensajeFinal != null)
{
txtMensajeFinal.gameObject.SetActive(true);
txtMensajeFinal.text = "¡Bien hecho!";
}
}

void ActualizarUI()
{
if (txtMonedas != null)
txtMonedas.text = monedasTotales + "/" + monedasObjetivo;
}

public void GastarMonedas(int cantidad)
{
monedasTotales -= cantidad;
if (monedasTotales < 0)
monedasTotales = 0;

PlayerPrefs.SetInt("MonedasTotales", monedasTotales);
PlayerPrefs.Save();

ActualizarUI();
}

// ==================== LLAVES ====================
public void AgregarLlave(string nombreLlave)
{
if (!llavesObtenidas.Contains(nombreLlave))
{
llavesObtenidas.Add(nombreLlave);
GuardarLlaves();

Debug.Log("Llave agregada: " + nombreLlave);
Debug.Log("Llaves actuales: " + string.Join(", ", llavesObtenidas));

ActualizarUILlaves();
}
else
{
Debug.Log("La llave ya estaba en el inventario: " + nombreLlave);
}
}

public bool TieneLlave(string nombreLlave)
{
return llavesObtenidas.Contains(nombreLlave);
}

void GuardarLlaves()
{
foreach (string llave in llavesObtenidas)
{
PlayerPrefs.SetInt("Llave_" + llave, 1);
}
PlayerPrefs.Save();
}

void CargarLlaves()
{
string[] llavesExistentes = { "LlavePuerta", "LlaveCofre", "LlaveOtra" };

foreach (string llave in llavesExistentes)
{
if (PlayerPrefs.GetInt("Llave_" + llave, 0) == 1)
{
if (!llavesObtenidas.Contains(llave))
llavesObtenidas.Add(llave);
}
}
}


void ActualizarUILlaves()
{
if (txtLlaves != null)
{
if (llavesObtenidas.Count == 0)
txtLlaves.text = "Llaves: (ninguna)";
else
txtLlaves.text = "Llaves: " + string.Join(", ", llavesObtenidas);
}
}
}

Escena Mapa 3 “Parkour”

Se creó una nueva escena y se le asignó el nombre Parkour, de esta forma es más fácil identificarla frente a las otras escenas en el momento de la exportación. Una vez la casa estuvo colocada en la vista Scene, se agregó un Terrain desde la ventana Hierarchy para comenzar a construir el entorno que rodearía la estructura.

Después de añadir el terreno, se ajustó su posición para alinearlo correctamente con la casa. Luego, desde el Inspector, en la ruta Terrain > Terrain Settings > Mesh Resolution (en Terrain Data), se modificó el tamaño del terreno para adaptarlo a las dimensiones necesarias del escenario. Con estos cambios, el terreno quedó acomodado y acorde a la ubicación y escala de la casa dentro de la escena.

Para modificar la estructura del terreno, desde el Inspector, dentro de la sección Terrain, se ingresó a la opción Paint Terrain y se configuraron las propiedades necesarias. Luego se seleccionó el pincel correspondiente y se procedió a moldear el terreno según las preferencias del escenario, ajustando alturas y formas conforme a lo requerido.

Para aplicar una imagen a un terreno en Unity, primero se selecciona el Terrain en el Hierarchy y en el Inspector se abre Paint Terrain y luego Paint Texture. Después se entra a Edit Terrain Layers y se crea una nueva capa con Create Layer, eligiendo la imagen que se quiere usar como textura. Unity la aplica de inmediato al terreno, y si se necesita ajustar cómo se repite, se modifican los valores de Tiling en la Terrain Layer. Si se quieren usar más texturas, simplemente se vuelve a Paint Texture y se pinta sobre el suelo.

Ahora, para hacer las plataformas donde el jugador va a saltar, desde la Hierarchy se agrega un objeto 3D de tipo Cubo. A este se le modificaron las dimensiones necesarias y se ubicó justo donde finaliza el terreno. Luego, desde el Inspector, se añadió el componente Rigidbody, configurándolo con Is Kinematic activado y Use Gravity desactivado. Después de esto, el cubo se arrastró hacia la ventana Project para convertirlo en un Prefab, y finalmente se eliminó de la escena para utilizar únicamente su versión prefab al momento de colocar las plataformas.

Para crear el prefab del Checkpoint (Anillo), se añadió un objeto desde GameObject > 3D Object > Cylinder y se nombró como CheckpointPrefab. Luego se ajustó la escala. Una vez configurado, se arrastró el objeto hacia la ventana Project para convertirlo en un Prefab, como en el paso anterior, y posteriormente se eliminó de la escena, quedando listo para colocarlo cuando fuera necesario.

Para generar automáticamente todas las plataformas del nivel, se creó un objeto vacío en la Hierarchy llamado ParkourLevelGenerator, que se ubicó estratégicamente cerca del inicio del jugador para que el recorrido comenzara en el lugar correcto. A este objeto se le asignó el script StagedParkourGenerator desde el Inspector, configurando todos los parámetros necesarios para que el generador funcionara correctamente. Entre estos parámetros se relacionaron los prefabs de las plataformas y de la llave, asegurando que cada elemento del nivel estuviera correctamente referenciado.

Adicionalmente, se añadieron dos objetos vacíos denominados StartParkour y FinalParkour, que sirven para definir con precisión el inicio y el final del camino del parkour. Estos objetos también se vincularon al script para que el generador pudiera calcular correctamente la posición y orientación de todas las plataformas intermedias. Gracias a esto, el recorrido tiene un punto de inicio y un punto final claros, garantizando que el jugador pueda avanzar de manera lineal y fluida a lo largo del nivel.

Dentro del generador se configuraron los valores enteros que controlan la distancia mínima y máxima entre plataformas, así como la variación en altura, lo que permite crear un recorrido dinámico y desafiante. De igual manera, se definieron las cinco etapas del nivel, asignando a cada una sus colores característicos, la probabilidad de incluir plataformas móviles o colapsables y el tipo de dificultad correspondiente. Esto asegura que el parkour aumente progresivamente en reto a medida que el jugador avanza, manteniendo el equilibrio entre diversión y desafío.

De esta manera, el ParkourLevelGenerator se convierte en la herramienta principal para estructurar y automatizar todo el recorrido del nivel. Su correcta configuración garantiza coherencia visual, mecánica y de dificultad en cada etapa del parkour, permitiendo que la escena se genere de manera automática y sin necesidad de colocar cada plataforma manualmente. Además, facilita realizar ajustes rápidos en la disposición, colores o comportamiento de las plataformas para probar distintas variantes del nivel.

En la primera etapa se definió un script que permitía establecer un estado por defecto para las plataformas, correspondiendo a plataformas estáticas que no presentan ningún tipo de movimiento ni cambio durante el recorrido. Este estado asegura que el jugador pueda familiarizarse con la mecánica básica del parkour antes de enfrentarse a desafíos más dinámicos.

La segunda etapa corresponde al script BlinkingPlatform.cs, donde las plataformas se desactivan después de cierto tiempo. Este script incluye la condición de que, si la plataforma anterior está desactivada, la siguiente debe permanecer activa para garantizar la continuidad del juego y evitar que el jugador quede bloqueado o caiga de manera inesperada.

La etapa siguiente utiliza el script SidewaysPlatform.cs, que genera plataformas que se mueven horizontalmente a cierta distancia. Este movimiento lateral añade un nivel de desafío mayor, obligando al jugador a calcular saltos y tiempos precisos para avanzar por el recorrido.

Finalmente, se definió un estado combinado llamado Combined, que integra las acciones de las dos etapas anteriores. En este modo, las plataformas se mueven de lado a lado y, al mismo tiempo, desaparecen después de un tiempo determinado, ofreciendo un reto más avanzado y dinámico que combina movimiento y temporización, incrementando la dificultad de manera progresiva.

De esta forma, el generador crea automáticamente las plataformas desde el punto inicial hasta el punto final, aplicando diferentes estados según la etapa correspondiente. Cada etapa define un comportamiento distinto de las plataformas, lo que permite que el recorrido sea dinámico y que la dificultad aumente progresivamente a medida que el jugador avanza. Gracias a esto, las plataformas no solo se colocan en la posición correcta, sino que también presentan variedad en su comportamiento y reto.

Para el siguiente paso, se definieron los checkpoints, que registran el progreso del jugador a lo largo del recorrido. Además, se implementó la disminución de vida cuando el jugador cae de las plataformas y los eventos de caída, asegurando que el sistema de respawn funcione correctamente y que el jugador pueda reaparecer en el último checkpoint alcanzado sin perder la coherencia del nivel.

Escena de créditos

Para la escena de créditos se utilizo la escena de la animación de bus que utilizamos en el videojuego, está escena aparecerá cuando el jugador supere el nivel de parkour e interactue con el bus. Aquí solo dejamos un texto estático pero la animación se muestra de forma exitosa de fondo. El bus al llegar a un box collider que predefinimos en el mapa, dirige al jugador al menú de inicio.

MODELOS DE SKETCHFAB A UTILIZAR

Modelo Bus

Autor del modelo: VRC-IW

Modelos de Árboles

Autor del modelo: Dari

Modelo Esqueletos

Autor del modelo: Daniel Geraldini

CONCLUSIONES

  • El uso del Canvas nos permitió estructurar de manera clara los elementos visuales del juego para el diseño de los menús interactivos para el usuario, incluyendo paneles, textos e indicadores. Su correcta implementación garantizó una interfaz intuitiva, facilitando que el jugador comprendiera la información y acciones disponibles durante la partida.
  • La interacción con el NPC nos ayudó a fortalecer la dimensión narrativa del juego al permitir diálogos, entrega de objetos y activación de eventos. Este sistema mostró cómo la comunicación entre jugador y personajes no jugables puede enriquecer la experiencia y guiar el progreso dentro del mundo del juego.
  • El sistema de recolección de monedas introdujo una mecánica básica de recompensa y progresión. Su implementación permitió trabajar conceptos de colisiones, prefabs y actualización dinámica de la interfaz, aportando un componente de motivación y objetivos claros dentro de la jugabilidad.
  • El diseño e implementación de un mapa de parkour buscó incentivar una movilidad más extrema, haciendo que el jugador ejecutara saltos, equilibrios y recorridos exigentes. Este elemento elevó la dificultad y añadió dinamismo a la jugabilidad, promoviendo destreza y precisión.

CRÉDITOS

Autor: 

Arlyn Jessenia Cortés García
Yerli Tatiana Urrea Naranjo

Editor: Carlos Iván Pinzón Romero

Código: UCMV-10

Universidad Central: Universidad Central

BIBLIOGRÁFIAS

BravePixelG. (2024, 14 junio). Cómo crear animaciones entre escenas más óptimas y rápidas en Unity (LeanTween para UI) [Vídeo]. YouTube. https://www.youtube.com/watch?v=CGuGb50py0g

DansterDev. (2021, 7 agosto). Como INTERACTUAR con objetos ¡RAPIDO Y SENCILLO! Unity 2021 [Vídeo]. YouTube. https://www.youtube.com/watch?v=oaWsRmDTLBo

DédaloLab. (2023, 2 febrero). Como HACER una IA ENEMIGA en UNITY 2D ✅ [Vídeo]. YouTube. https://www.youtube.com/watch?v=w4unf6meEvI

DxD - Programación y Diseño. (2023, 27 mayo). COMO EXPORTAR DE BLENDER a UNITY EN 2023 [Vídeo]. YouTube. https://www.youtube.com/watch?v=i1NGjQvNqn0

GDT Solutions ES. (2021, 20 julio). EXPORTAR modelo 3D con TEXTURAS de BLENDER A UNITY [Vídeo]. YouTube. https://www.youtube.com/watch?v=WGQ1nt05WnA

Technologies, U. (s. f.-a). Escenas - Unity Manual. https://docs.unity3d.com/es/2018.4/Manual/CreatingScenes.html

Technologies, U. (s. f.-b). GameObjects - Unity Manual. https://docs.unity3d.com/es/2018.4/Manual/GameObjects.html

Technologies, U. (s. f.-c). Lights (luces) - Unity Manual. https://docs.unity3d.com/es/2018.4/Manual/Lights.html

Technologies, U. (s. f.-d). Prefabs - Unity Manual. https://docs.unity3d.com/es/2018.4/Manual/Prefabs.html

Technologies, U. (s. f.-e). UI (Interfaz de Usuario) - Unity Manual. https://docs.unity3d.com/es/2018.4/Manual/UISystem.html

VOID. (2024, 8 agosto). Creación de un sistema de interacción parte 2 | Tutorial Unity [Vídeo]. YouTube. https://www.youtube.com/watch?v=kjPnsI5THu0