Información actualizada sobre esta entrada
Este artículo pertenece a una serie que consiste en hacer un juego simple en primera persona acerca de encontrar objetos dentro de un laberinto. Es una de mis primeras series de cuando empecé el canal, ahora he mejorado mucho este proyecto y puedes descargar el código fuente para importarlo en tu propio proyecto de Unity. Algún día vamos a hacer un remake de esta serie, suscríbete a mi canal para estar al tanto del nuevo contenido sobre Blender, Unity y programación.
Sígueme en itch.io y descarga el código fuente de este proyecto
PUEDES TESTEAR ESTE JUEGO AQUÍ. TAL VEZ TARDE UN POCO EN CARGAR
🔻
MOVEMENT: WASD CAMERA LOOK: MOUSE
Introducción al artículo original
En este artículo vamos a ver una forma de colocar objetos colectables en Unity, es decir objetos que el personaje puede recolectar y estos tendrán un efecto en él. Estos objetos serán relojes que al recogerlos nos sumarán tiempo en la cuenta regresiva.
Vamos a utilizar el prefabricado del reloj que configuramos en el segundo artículo del proyecto, clic aquí para descargar los archivos y ver cómo configurar los prefabs.
Vídeo relacionado a este artículo
También te dejo este video en el que hablo sobre Generación de Datos Aleatorios en Unity, está dividido en dos partes, en la primera vemos cómo generar valores enteros y reales dentro de uno o más intervalos y en la segunda parte vemos cómo generar otras estructuras de datos como Vector2, Vector3, Quaternion y Color.
Descripción del objetivo
Debemos establecer reglas y hacer una descripción exhaustiva del comportamiento de los relojes, por ejemplo cómo será la interacción con el personaje, cómo aparecerán en el escenario, etc. Cuanto mejor sea la descripción, más fácil será crear una solución usando programación.
Lo que buscamos es que en el laberinto aparezca un determinado número de relojes. Debemos asegurarnos de que estos relojes no aparezcan dentro de los muros y no se superpongan entre si.
Para lograr esto voy a reutilizar la solución del artículo anterior para colocar el pedestal en una posición aleatoria, en la solución hacía que cada pieza del laberinto conozca su propia geometría y nos dé una posición de su interior si se lo pedimos.
Además voy a hacer que cada pieza del laberinto pueda contener un sólo reloj, de esta forma no corremos el riesgo de colocar dos relojes superpuestos.
Para lograr esto debemos llevar un registro de las piezas del laberinto que tienen un reloj en su interior, de esta manera cuando vamos a colocar un nuevo reloj, esas piezas no serán consideradas en la selección.
Cuando el personaje tome un reloj se agregará una determinada cantidad de segundos a la cuenta regresiva.
Cada vez que un reloj es destruido aparece uno nuevo en el escenario, de esa forma todo el tiempo tendremos la misma cantidad de relojes en el escenario.
Resolución
Pasos previos
Comenzamos seleccionando el GameObject Control de la jerarquía y asignándole el tag «GameController». Luego seleccionamos el GameObject FPSController y le asignamos el tag Player.
A continuación vamos a crear un nuevo Script llamado Clock, el cual posteriormente asignaremos al prefabricado del reloj.
Este Script modelará el comportamiento de los relojes.
Campos de Script Clock
Vamos a definir un String serializado que llamaremos «playerTag», esta variable contendrá simplemente el nombre del tag que tenemos asignado en el jugador.
Luego definiremos una variable tipo float para indicar el tiempo de vida en segundos del reloj en el escenario.
Definimos un GameObject llamado labyrinthPiece con «get» y «set» como se indica en la figura 5, de esta forma será un parámetro que puede ser leído y escrito. Usualmente me gusta definir métodos públicos para acceder a las variables, pero esta es una forma muy práctica de hacerlo.
Por último definimos un objeto tipo GameControl ya que el reloj necesita informar al control lo que está ocurriendo.
Métodos de Script Clock
En primer lugar definimos el método público SetLifeTime con parámetro float que usaremos para que el objeto GameControl le asigne un determinado tiempo de vida al reloj.
Luego tenemos el método SelfDestruction que se ejecutará cuando se acabe el tiempo de vida del reloj o el personaje lo agarre.
El método Collect se ejecutará cuando el personaje tome el reloj, se encargará de avisar al GameControl y luego autodestruirse.
Por último el método OnTriggerEnter para detectar al personaje, cuando esto ocurre vamos a ejecutar el método Collect.
Campos en GameControl
Vamos al Script GameControl y definimos cuatro campos serializados.
ClockPrefab contendrá el prefab del reloj que colocaremos en el escenario. El entero nClocks indicará cuántos relojes se deben colocar en el escenario. El float clockLifetime será el tiempo de vida medio de un reloj en el escenario. Por último el entero timePerClock indicará cuántos segundos se añaden al Timer luego de que el personaje toma un reloj.
Declaración de métodos del Script GameControl
El método PlaceAllTheClocks se encargará de asegurarse de que es posible poner la cantidad de relojes indicada y luego ejecutar el método PlaceAClock tantas veces como relojes haya que colocar.
ClockDestroyed será ejecutado por un reloj que acaba de autodestruirse, de esa forma el Script GameControl podrá volver a considerar la pieza del laberinto donde el reloj estaba y colocar un nuevo reloj en una posición aleatoria.
ClockCollected será ejecutado por un reloj cuando el personaje entra en contacto con él, de esta forma podremos agregar tiempo al temporizador.
Por último el método DestroyAll se encargará de destruir todo lo que deba ser destruido al finalizar una partida (pedestal, relojes, personaje, etc).
Instrucciones de los métodos de Clock
Para empezar en el método Start de Clock encontramos la referencia del objeto GameControl, luego invocamos el método «selfDestruction» en un tiempo que es el resultado de la suma del tiempo de vida del reloj con un valor aleatorio entre 0 y 1. De esta forma logramos que los relojes no se destruyan en el mismo frame.
En el método SelfDestruction vamos a informar al objeto GameControl que un reloj fue destruido y pasamos como parámetro la pieza del laberinto asignada al reloj, de esa forma el objeto GameControl podrá sacarla de la lista de exclusión. Luego ejecutamos el método Destroy con parámetro «gameObject» para que el objeto se destruya a si mismo.
En el método Collect primero vamos a cancelar la invoación pendiente del método selfDestruction. Luego informamos al objeto GameControl que un reloj fue recolectado. Finalmente ejecutamos el método selfDestruction.
En el método OnTriggerEnter vamos a preguntar si el tag del Collider que tocó el reloj es el del jugador y si esto es verdadero vamos a ejecutar el método Collect.
Para ver en detalle cómo funciona el método OnTriggerEnter y OnTriggerEnter te invito a ver uno de los videos de la serie fundamental de Unity que hice hace un tiempo. Si prefieres información más detallada aquí está el artículo de ese video.
Instrucciones de los métodos de GameControl
Método PlaceAllTheClocks
En el método PlaceAllTheClocks de GameControl vamos a leer la cantidad de piezas de laberinto que tenemos disponibles para colocar los relojes. Si resulta que se deben poner más relojes que la cantidad de piezas de laberinto, vamos a hacer que nClocks sea igual al número de piezas de laberinto menos uno, de esta forma evitaremos que el programa entre en un bucle infinito.
Luego haremos un loop ejecutando el método PlaceAClock para colocar un reloj en el escenario.
Método StartGame
En el método StartGame vamos a crear el objeto List de GameObjects (línea 182 de la figura 14).
Crear los objeto es muy importante, tanto así que en un examen de informática en el que había que escribir código en papel, se me olvidó colocar la instrucción new para crear el objeto y me restaron casi 40 puntos de 100. Cuando fui a defender mi examen consideraron que habían exagerado un poco dado que había sido mi único error y me perdonaron.
Método PlaceAClock
Volviendo al tema de los métodos, en PlaceAClock tenemos que elegir aleatoriamente una pieza del laberinto asegurándonos que la pieza no contiene ya un reloj, una vez que la conseguimos, le pedimos una posición aleatoria de su interior. Para resolver esto es necesario haber resuelto el ejercicio del artículo anterior, en el que creamos el Script Labyrinth Piece.
El algoritmo para colocar la pieza del reloj se puede ver en la figura 15.
Método ClockDestroyed
En el método ClockDestroyed vamos a remover de la lista la pieza del laberinto que nos pasan como parámetro y luego ejecutar el método PlaceAClock para colocar un nuevo reloj en el escenario.
Método AddSeconds de Timer
Necesitamos definir un método público dentro de Timer que nos permita añadir una cantidad de segundos al temporizador.
Al final del Script hacemos la declaración del método AddSeconds, lo completaremos al final.
Volviendo al Script GameControl, en el método ClockCollected hacemos la llamada al método AddSeconds de timer, pasando como parámetro el entero timePerClock.
Ahora vamos al método EndGame, en donde están las líneas Destroy vamos a ejecutar el método DestroyAll y cortar las dos instrucciones Destroy que habíamos colocado previamente en otros artículos.
Esas dos instrucciones las pegamos en el método DestroyAll y luego econtramos todos los relojes presentes en el escenario y los destruimos usando un bucle foreach.
Configurar prefabricado del reloj
Ahora vamos a seleccionar el prefabricado del reloj de la carpeta del proyecto y arrastrarlo al escenario para configurarlo.
Creamos el tag Clock y se lo asignamos al GameObject del reloj.
Luego arrastramos el Script Clock hacia sus componentes o utilizamos el botón AddComponent. Luego introducimos «Player» en el campo del tag (figura 25).
Vamos al GameObject Control e ingresamos los nuevos parámetros que habíamos definido previamente.
Ahora vamos a completar el método AddSeconds del Script Timer que nos había quedado pendiente.
Dentro simplemente vamos a incrementar los segundos, ajustar los minutos y ejecutar el método WriteTimer para actualizar los valores.
Error de programación
En este punto apareció un error en la consola diciendo que no existe una versión del método PlaceAClock que no lleve parámetros.
Voy a la línea 184 del Script GameControl donde apareció el error, en efecto vemos en la figura 29 que la ejecución del método PlaceAClock se hace sin parámetros.
Defino el entero nPieces con el valor de la cantidad de elementos en la lista de piezas del laberinto e ingreso este entero como parámetro del método.
Corrección de Bug
Cuando probé el juego no parecían haber aparecido los relojes en el escenario, al pausar la simulación y buscar en la jerarquía descubrí que habían aparecido pero estaban boca abajo como se ve en la figura 31.
Para corregir este bug voy al método donde se coloca un reloj en el escenario y busco la instrucción en la que hago el Instantiate, instrucción 173 en la figura 32.
En lugar de darle al nuevo GameObject la rotación del Quaternion identidad voy a darle la rotación que viene definida en el prefab del reloj, el cual cuando colocamos en el escenario aparece correctamente orientado.
Últimos detalles y prueba
Al probar el juego noté que había pocos relojes y que estos entregaban muy poco tiempo al recogerlos.
Para balancear correctamente los elementos es necesario hacer varias pruebas y ver qué funciona mejor, además esto puede formar parte de un sistema de dificultad en el cual una dificultad alta implica relojes menos frecuentes que entregan poco tiempo al recogerlos.
En la figura 35 tenemos dos relojes frente a nostors y el timer marca aproximadamente un minuto cincuenta, la figura 36 fue tomada momentos despues de agarrar los dos relojoes, vemos que el Timer ahora marca un poco más de dos minutos diez. Con esto damos por concluido el problema.
Conclusión
En este artículo hemos visto cómo colocar colectables aleatoriamente en Unity, estos colectables eran los relojes que al recogerlos debían sumar tiempo a la cuenta regresiva.
El objeto GameControl es el que se encarga de colocarlos en el escenario aleatoriamente haciendo uso de la solución creada en el artículo anterior para colocar el pedestal aleatoriamente en una pieza del laberinto.
Para resolver el problema hemos creado un Script que modelará el comportamiento del reloj y que intercambiará mensajes con los demás Scripts para informar de eventos como una autodestrucción o que el personaje ha recogido el reloj.
El efecto de elemento colectable lo hacemos simplemente ejecutando acciones apropiadas cuando el personaje pasa por encima de ellos.