#10 Colocar colectables en Unity aleatoriamente

Por GameDevTraum

Introducción

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.

Página principal del proyecto

En el siguiente video puedes ver cómo se resuelve el problema de colocar prefabricados de manera aleatoria en el escenario.

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.

reloj colocado aleatoriamente en unity
Fig. 1: Los relojes aparecerán en posiciones aleatorias del laberinto.

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.

asignacion de tags en unity desde la ventana inspector
Fig. 2: Seleccionamos el objeto control y asignamos el tag “GameController”.

asignacion de tags en unity desde la ventana inspector
Fig. 3: Seleccionamos el prefab del jugador 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.

creacion de scripts c# en unity
Fig. 4: Creamos un nuevo Script con nombre “Clock”.

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.

script c# en unity para controlar la aparicion aleatoria de relojes en el escenario
Fig. 5: Definimos estos campos para utilizar en la solución.



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.

script c# en unity para controlar la aparicion aleatoria de relojes en el escenario
Fig. 6: Definimos estos métodos para utilizar en la solución,

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.

campos de la clase gamecontrol que se usaran para resolver el problema
Fig. 7: En GameControl vamos a definir algunos campos para resolver el problema.



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

metodos de la clase gamecontrol que se usaran para resolver el problema
Fig. 8: Definimos algunos métodos en GameControl.

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.

metodo start del reloj, encontrar referencias, invocar autodestruccion
Fig. 9: En el método Start de Clock vamos a encontrar la referencia de la componente GameControl e invocar la autodestrucción.

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.

metodo auto destruccion del reloj
Fig. 10: En SelfDestruction vamos a comunicar al Control que el reloj fue destruido y luego ejecutar el método Destroy.

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.

metodo collect para un reloj que aparece en el escenario aleatoriamente
Fig. 11: El método Collect informa al control del evento y luego se autodestruye.

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.

metodo ontriggerenter para un reloj que aparece en el escenario aleatoriamente
Fig. 12: Si el Collider que entra en contacto con el reloj tiene el tag “Player” ejecutaremos el método Collect.



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.

metodo place all the clocks para colocar todos los relojes aleatoriamente en el escenario
Fig. 13: En el método PlaceAllTheClocks pondremos todos los relojes 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.

metodo start de gamecontrol, juego del laberinto en unity
Fig. 14: En Start de GameControl creamos el objeto lista y luego ejecutamos el método PlaceAllTheClocks.

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.

metodo place a clock para colocar un reloj aleatoriamente en el escenario
Fig. 15: El método PlaceAClock se encargará de elegir una pieza del laberinto y colocar el reloj.



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.

metodo clock destroyed de gamecontrol para informar que un reloj ha sido destruido
Fig. 16: En Clock Destroyed eliminamos la pieza del laberinto que contenía el reloj de la lista y colocamos un nuevo reloj.

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.

metodo add seconds de timer para agregar segundos a la cuenta regresiva
Fig. 17: En el Script Timer vamos a agregar un método público para poder añadir segundos al timer.

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.

metodo clock collected de game control para informar que se ha recogido un reloj y se debe sumar segundos al timer
Fig. 18: En el método Clock Collected vamos a añadir segundos a la cuenta regresiva.

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.

metodo end game para realizar todas las acciones al finalizar una partida
Fig. 19: Vamos al método EndGame, cortamos las instrucciones de destrucción y ejecutamos el método DestroyAll.

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.

metodo destroy all para destruir todo lo que sea necesario al finalizar una partida en el juego del laberinto
Fig. 20: En DestroyAll pegamos las instrucciones cortadas anteriormente y eliminamos todos los relojes del escenario.



Configurar prefabricado del reloj

Ahora vamos a seleccionar el prefabricado del reloj de la carpeta del proyecto y arrastrarlo al escenario para configurarlo.

prefab de elemento colectable en unity, los relojes aparecen aleatoriamente en el escenario
Fig. 21: Seleccionamos el prefab del reloj.

Creamos el tag Clock y se lo asignamos al GameObject del reloj.

creacion de tags en unity
Fig. 22: Creamos un nuevo Tag llamado Clock.

asignacion de tags en unity desde la ventana inspector
Fig. 23: Al prefab del reloj le asignamos el tag Clock.

ventana inspector de un reloj que aparece en el escenario aleatoriamente en el juego del laberinto
Fig. 24: Reloj con el tag asignado.

Luego arrastramos el Script Clock hacia sus componentes o utilizamos el botón AddComponent. Luego introducimos “Player” en el campo del tag (figura 25).

gameobject colectable en unity
Fig. 25: Asignamos el Script Clock al prefab del reloj e introducimos el Tag del jugador.

Vamos al GameObject Control e ingresamos los nuevos parámetros que habíamos definido previamente.

ventana inspector del game object control que se encarga de control el juego del laberinto, parametros para colocar elementos colectables en unity
Fig. 26: Seleccionamos el objeto Control y en el inspector seteamos los nuevos parámetros.

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.

metodo add seconds de timer para agregar segundos a la cuenta regresiva
Fig. 27: Volvemos al Script Timer y completamos el método AddSeconds.

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.

error visualizado en consola de unity
Fig. 28: Surge un error que dice que no hay una definición del método 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.

correccion de bug en script game control del juego del laberinto en unity
Fig. 29: Voy a la instrucción en la que está el error.

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.

correccion de bug en script game control del juego del laberinto en unity
Fig. 30: Defino un entero con la cantidad de piezas de laberinto y se lo paso como parámetro al 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.

bug en el que los elementos colectables en unity aparecen mirando boca abajo en el juego del laberinto
Fig. 31: Al correr el juego encontramos un bug, los relojes aparecen boca abajo.

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.

solucion del bug en el que los relojes aparecen mirando boca abajo en el juego del laberinto
Fig. 32: Voy al punto donde coloco los relojes en el escenario.

solucion del bug en el que los relojes aparecen mirando boca abajo en el juego del laberinto
Fig. 33: En lugar de usar Quaternion.Identity utilizo la rotación que viene definida en el Prefab.

Ú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.

ventana inspector del game object control que se encarga de control el juego del laberinto, parametros para colocar elementos colectables en unity
Fig. 34: Ajusto los valores en GameControl para que me den más 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.

escena del juego del laberinto en el que hay dos relojes para recoger y obtener tiempo
Fig. 35: En la escena se observan dos relojes frente al personaje y el tiempo indica 1:47.

escena del juego del laberinto
Fig. 36: Luego de recoger ambos relojes, el tiempo es 2:13.



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.