The Magicians Nook


Tipo de proyecto:

Proyecto de la Universidad / Desarrollado individualmente

Desarrollado en:

Unity 6 / Programado en C#

Mi rol en el proyecto:

Programador general / Gameplay / UI / Arte


Mecánicas base de personaje

Para esta demo desarrollé un sistema de movimiento 2D como un Plataformas, con controles típicos y funcionalidad, moviéndose fluidamente en cualquier dirección tanto horizontal como en el aire, teniendo animaciones de transición que lo hacen ver suave y natural, además de esto tiene la posibilidad de atacar en cualquier estado de movimiento, logrando un control fluido y sin pausas a la hora de jugar en las reacciones del personaje.

El sistema de movimiento inicia tomando el Axis Horizontal del jugador mediante el input en el Update, después realiza una comparación de si el jugador presionó el salto, si esto es verdadero entra una vez y vuelve la variable verdadera, luego compara constantemente si el jugador está en contacto con el suelo usando un OverlapBox y llama a otras funciones de comportamiento adicionales. Además de un Update este código cuenta con un FixedUpdate que ejecuta la función de Movimiento del personaje así como el control del salto, por último vuelve falsa la variable de SaltoPulsado.

Dentro de las funciones de movimiento la llamada MoverJugador, que se ejecuta en el FixedUpdate, esta al iniciar compara si el jugador no está en el suelo y si tiene bloqueado moverse en el aire, si ambos son falsos regresa y no se ejecuta la funcionalidad de movimiento, después si esto no se cumple se crea un Vector3 con la nueva velocidad, que luego se aplica al RigidBody usando la función SmoothDamp para suavizar el movimiento, por último se compara el valor de la velocidad del jugador y hacia que dirección está mirando con el Bool MirandoDerecha para hacerlo girar a la dirección opuesta. La función ControlSalto realiza la misma comparación, y si el personaje no está en el suelo o no se ha pulsado el botón de salto hace un Return, si esto no se cumple llama a la función Saltar.

Siguiendo con el movimiento del jugador, la función ControlAnimaciones simplemente se encarga de enviar la información a los Animator del personaje, utilizando los valores del RigidBody y otras condiciones que se actualizan en su Script, esto es debido a que la animacion está separada en dos componentes, uno para las manos y uno para el cuerpo que funcionan de manera independiente para conseguir más fluidez en el juego. La función Saltar únicamente vuelve la variable SaltoPulsado falsa y utilizando la función AddForce añade una fuerza al RigidBody utilizando la fuerza del salto a modo de impulso, por último la función Girar invierte la variable booleana MirandoDerecha y rota al personaje al sumar 180 grados en su Transform Y mediante EulerAngles.

Al tratarse de un videojuego de Plataformas el jugador también puede recibir daño, en este caso la función RecibirAtaque aplica para cuando es atacado por enemigos directamente, aquí inicia comparando si el jugador en inmune al daño, si esto es verdadero hace un Return, si es falso hace un Trigger en la animación para indicar que el jugador recibió daño a modo de Feedback, luego resta la vida al jugador según el ataque y cambia el UI con el nuevo valor de vida, por último compara si la vida es igual o menor a 0 y si esto es verdadero el personaje es destruido. La función RecibirEmpuje se aplica al mismo tiempo que RecibirAtaque, siendo llamada por los enemigos si su tipo de ataque es de empuje, aquí se hace una comparación para saber la dirección del jugador, aplicando el empuje con una fuerza contraria hacia donde mira, es decir, hacia atrás, luego de esto inicia dos Corrutinas para controlar su movimiento y reaccionar al ataque.

Terminando con las funciones de movimiento la Corrutina PerderControl cambia el valor del Bool que determina que le jugador puede moverse y después de un determinado tiempo reestablece esto, haciendo que el jugador no pueda moverse mientras está siendo atacado o cuando es afectado por control de un enemigo. La Corrutina Inmunidad realiza la misma lógica que la anterior, pero esta vez con el Bool que impide que el jugador reciba daño, necesario para que no se acumulen demasiados inputs de ataque que hagan que el jugador no tenga forma de moverse o escapar y que reciba demasiado daño en poco tiempo. Por último la función EstadoDeGravedad se activa cuando el jugador está en el aire, aumentando la gravedad según el tiempo que lleva en el aire, haciendo que caiga con más peso para dar una mejor sensación, si el jugador no está en el aire establece la fuerza de gravedad con su valor base para el jugador.

La funcionalidad de ataque es bastante sencilla, iniciando con el Update que compara si el jugador puede atacar, si esto es verdadero se compara el input de atacar, si el jugador presiona Fire1, en este caso el click izquierdo, revisa en un If si ya está atacando y si esto es falso llama la función IniciarAtaque, que activa el Trigger del Animator y hace un Invoke a LanzarCarta, aquí se compara a que dirección está mirando el jugador para obtener la carta que lanzará según esto, determinada con un RandomRange en el Array de todos los tipos de carta, por último instancia la carta en el punto de disparo y le establece el valor de ataque según el que tiene el jugador.

Las cartas lanzadas por el jugador funcionan a modo de proyectil para dañar a los enemigos al contacto, su funcionamiento es bastante simple iniciando en el Start que ejecuta Destroy para destruir la carta después de 5 segundos, en el Update hace un Translate para avanzar hacia su derecha según su velocidad. Por último el OnTriggerEnter se encarga de detercar el contacto con el enemigo comparando su Tag, entonces toma su Script y llama a la función RecibirAtaque pasándole el daño de la carta para aplicarlo, al hacer esto la carta se autodestruye.


Enemigo y comportamiento

Algo fundamendal en un videojuego de Plataformas son los enemigos, para esto desarrollé un enemigo con múltiples comportamientos y ataques al jugador en diferentes situaciones que supone un gran riesgo al jugador, pudiendo estar en Idle, patrullaje, seguimiento y ataque básico y especial.

El enemigo comienza con un Enum que define sus estados de movimiento y ataque, después simplemente en el Start obtiene la posición y rotación iniciales de la criatura para guardarlas en variables.

El Update del enemigo inicia comparando si este está ejecutando el ataque especial, si esto es falso entra al If para reducir el CoolDown del siguiente ataque, después pasa un Float al Animator para la velocidad que controla sus animaciones con un Mathf.Abs. Por último cambia de comportamiento usando un Switch para cada valor del Enum, que llama a la función para ejecutar este estado y su funcionalidad según el caso.

Iniciando con los comportamientos básicos de movimiento, la función Idle que establece la velocidad del RigidBody a 0 y apaga las partículas del ataque especial, después el estado Patrullando igualmente apaga las partículas y crea un Raycast desde un punto de control hacia abajo para detectar el suelo y saber si puede o no seguir caminando, después agrega velocidad al RigidBody hacia su derecha, por último se realiza una comparación y si la criatura deja de detercar suelo llama a la función para girar hacia el otro lado. El estado Siguiendo inicia comparando si la criatura puede moverse, si esto es verdadero sigue y aumenta la velocidad, creando el mismo Raycast del suelo, también llama a la función MirarObjetivo para orientar su dirección a donde mira, para la comparación de suelo llama a la misma función de Girar, apagando las partículas y estableciendo el comportamiento como Volviendo.

El estado Volviendo inicia llamando a la función MirarObjetivo para orientar su dirección y aplicando velocidad al RigidBody, después compara si llegó a la posición inicial Mediante un Distance y compara si debe patrullar para activar el estado Patrullando, si esto es falso establece su rotación inicial y compara este valor para cambiar la variable MirandoDerecha para saber a que dirección apunta, por último llama al estado Idle.

Para el ataque la función EstadoAtaqueBasico que establece la velocidad en 0 y la variable de movimiento fomo falsa, el EstadoAtaqueEspecial también establece la velocidad en 0 y llama a la función MirarObjetivo, encendiendo las partículas del ataque especial. Por último el EstadoAtaqueEnMovimiento enciende las partículas, mira al objetivo y aumenta su velocidad mientras usa el mismo Raycast de movimiento, si este deja de detectar el suelo se llama a la función Girar y se establece un Bool para el Animator, reiniciando el CoolDown de ataque especial y estableciendo el estado a Volviendo.


Recolección de items y puntos

Dentro de un videojuego de Plataformas la obtención de puntos y recolección de objetos es algo muy importante, para este demo desarrollé un sistema de objetos random en cada partida, cada uno con su propios valores de puntuación y con un sprite diferente para representar estos cambios.

Este sistema inicia con el Awake que genera un número aleatorio con un RandomRange entre 0 y el total de los objetos del Array de recolectables, y después llama a la función AsignarCarta. La detección de contacto con el jugador para ejecutar la funcionalidad se realiza con OnTriggerEnter que compara al jugador para destruir este objeto. Por último en OnDestroy llama al GameManager para sumar los puntos, pasando el valor determinado previamente para este objeto.

La función AsignarCarta tiene un Switch, que utiliza el index generado en el Awake para en cada Case asignar el valor de puntos al objeto, así como establecer su sprite y su animación, esta función tiene el total de casos para el tamaño del Array de posibilidades del item.


Sistema de combate por turnos

Además de ser un videojuego de Plataformas, The Magicians Nook también es un RPG de combate por turnos, por lo que desarrollé este sistema en combates 1vs1 donde cada combatiente tomal el control de una criatura que atacará según indique el jugador y la IA del enemigo, tomando en cuenta valores de vida, daño, defensa, así como habilidades que potencian las estadísticas del combatiente y factor crítico.

Todo inicia con la clase Stats, la cual indica las estadísticas y atributos de cada combatiente en las que basar sus ataques y sus reacciones, entre estos valores se encuentra el nivel, vida y vida máxima, ataque, defensa. Dentro de la clase Stats tiene una función pública con el mismo nombre que recibe los mismos datos, llamada por cada combatienete para cargar sus propios valores, la función Clon se encarga de una variante de los Stats originales, esto aplica cuando se usan habilidades que alteran directamente los valores base.

La clase Combatiente inicia asignando los Stats que posee al panel de UI en el Start, después establece sus habilidades obteniendo su Script de Habilidad mediante un GetComponentInChildren, por último crea una copia de sus estadísticas originales que serán alterados según el caso. La función ActualizarVida sirve cuando el combatienete recibe un ataque que altera su vida, primero establece la vida con un Clamp que resta el valor de cambio entrante con el mínimo en 0 y la vida máxima, luego usando Mathf.Round convierte el valor de vida en entero para no mostrar decimales y al final asigna la nueva cantidad de vida en el panel de UI. La función ObtenerStatsActuales se usa para recalcular y devolver los nuevos Stats cuando alguno de estos valores cambia, creando una instancia local de sus Stats y por cada uno establecer un nuevo dato según la entrada para los cambios, por último regresa estos nuevos Stats. La función EstaVivo se usa para comparar si la vida actual del combatiente es mayor a 0 para saber si murió.

Dentro de la clase Combatiente existe una clase definida para el jugador, esta inicia declarando sus nuevas Stats en el Awake, además de esto contiene dos funciones que son llamadas por otros Scripts para hacer funcionar el combate, la función IniciarTurno sobreescribe a la del GestorDeCombate, aquí inicia activando el UI del panel de habilidades del jugador, y después asigna un nombre a cada botón al activarlo para mostrar las acciones que puede realizar la criatura con la que combate el jugador. La función EjecutarHabilidad oculta el panel de habilidades y después asigna la habilidad pulsada como la habilidad actual, luego mediante el Script de la clase Habilidad llama a la función AsignarLados que a su vez llama al GestorDeCombate para que ejecute EstablecerOponente, todo esto sirve para determinar a quien va dirigida esta habilidad, ya que puede ser un ataque o un Boost para sí mismo, por último llama al GestorDeCombate nuevamente para que aplique la función EjecutarHabilidad pasando la habilidad actual.

Al igual que una clase de Combatiente para el jugador, existe una para el enemigo que, al no tener un UI de habilidades es más simple, esta inicia declarando sus Stats en el Awake, después sobreescribe la función IniciarTurno pasando su IA, por último hay una Corrutina llamada IA que controla las acciones del enemigo, al entrar a la operación espera 1 segundo y establece una habilidad para ejecutar de manera random utilizando un RandomRange entre el total de habilidades de la criatura enemiga, después mediatne el Script de la habilidad llama a la función AsignarLados y EstablecerOponente para el GestorDeCombate, por último le indica al GestorDeCombate que se está ejecutando una habilidad que pasa desde aquí.

El Script de habilidad tiene varias funciones de gestión e información para las habilidades, iniciando con la función Animar que se encarga de las animaciones de ataque de las criaturas, comparando el tipo de ataque de la habilidad ejecutada y aplicando un Trigger al Animator correspondiente. La función Iniciar se llama al iniciar una habilidad, aquí compara si esta es dirigida a si mismo y se establece como receptor, después llama a la función Animar y EnEjecucion.

Dentro de Habilidad está la función AsignarLados, llamada por las clases de Combatiente para establecer al emisor y receptor de la habilidad a ejecutar que toma sus valores al ser llamada. La función SiguienteMensaje igualmente se llama al iniciar una habilidad, comparando si hay mensajes para mostrar en la consola y en caso de haber 1 mensaje limpiar el Queue, si no hay ningún mensaje en cola retorna Null.

La clase AlterarVida pertenece a la clase Habilidad, esta representa el tipo de habilidad que tiene impacto en la vida del oponente, es decir que es un ataque, esta clase inicia declarando un Enum con los 3 tipos de daño que pueden aplicarse, primero se declaran algunas variables como el daño, el tipo de ataque, y la probabilidad de crítico, después la función EnEjecucion establece el valor con el que se cambiará la vida del receptor y se obtiene un número random entre 0 y 1 con un RandomRange para determinar el crítico, entonces se compara si este valor es igual o menor a la probabilidad de crítico de la habilidad y duplica el daño mostrando un mensaje en la consola de combate, por último llama a la función ActualizarVida del receptor pasando el ataque.

La función RecibirModificacion hace un Switch según el tipo de ataque recibido, siendo el primer caso para un ataque basado en Stats, es decir que toma en cuenta valores como el nivel, defensa y ataque del emisor y receptor de la habilidad para determinar el daño final que se aplicará, para esto primero se obtienen los Stats para ambos combatienetes y se calcula el ataque bruto utilizando la fórmula de otros juegos RPG, luego se aplica otra operación a este resultado y se hace un Return con este valor. Para el caso de una habilidad de daño fijo simplemente se hace un Return del valor de ataque de forma directa. La última opción es si la habilidad está basada en un porcentaje de la vida del receptor, obteniendo sus Stats de vida máxima y haciendo un Return de este valor por el porcentaje de vida que resta la habilidad aplicada, en caso de que alguno de estos casos falle o de que ocurra un error se envía un mensaje de que la operación es inválida.