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.

Página principal del proyecto

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.

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.

Fig. 2: Seleccionamos el objeto control y asignamos el tag «GameController».

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.

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.

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.

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.

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

Fig. 21: Seleccionamos el prefab del reloj.

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

Fig. 22: Creamos un nuevo Tag llamado Clock.

Fig. 23: Al prefab del reloj le asignamos el tag Clock.

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

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.

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.

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.

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.

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.

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.

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.

Fig. 32: Voy al punto donde coloco los relojes en el escenario.

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.

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.

Fig. 35: En la escena se observan dos relojes frente al personaje y el tiempo indica 1:47.

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.

Salir de la versión móvil
Secured By miniOrange