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 estrategia para colocar un objeto en una posición aleatoria del laberinto usando el método Random.Range y el prefab del pedestal con la espada que se creó en el segundo artículo del proyecto, clic aquí para descargar los archivos y ver cómo configurar los prefabs.
No podemos colocar el objeto en cualquier posición elegida al azar, debido a las paredes del laberinto, que quizás en el futuro nos interese generarlo de manera procedural. Así que para lograr el objetivo debemos analizar la geometría de las piezas y proponer una solución acorde.
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 problema
Necesitamos que el pedestal con la espada incrustada que se observa en la figura 1, pueda aparecer en cualquier parte del laberinto.
En el video 4 de la serie resolvimos un problema similar para colocar al personaje en una de las puertas al iniciar el juego. Aquí dejo el artículo por si quieres echar un vistazo.
En el video 4 utilizamos empty GameObjects para designar posiciones puntuales en las cual colocar el prefab del personaje.
En este caso estamos buscando una solución un poco más compleja, primero vamos a elegir una de las piezas que componenen el laberinto. La pieza que elijamos será un duplicado de alguna de las piezas que se observan en la figura 3.
No basta solo con elegir la pieza, necesitamos obtener una posición dentro de ella y esta posición debe ser dentro de una región en la que se pueda caminar. Estas regiones las vemos en la figura 4.
Al analizar el problema vemos que tendremos que tomar varias decisiones aleatorias y sería deseable que la solución que propongamos sea independiente de la cantidad de piezas que tenga el laberinto. Es decir si agregamos más piezas de la figura 4, estas se agreguen automáticamente al sistema de selección.
Estrategia propuesta – Definir segmentos
Dada una de las piezas de la figura 4, somos capaces de trazar una o dos líneas internas por donde el jugador puede circular dentro de la pieza.
Consideremos los extremos de estas dos líneas. En la figura 6 están identificados cuatro puntos que serían los extremos de estos dos segmentos.
Estos puntos podríamos representarlos con Empty GameObjects como hijos de la pieza.
En la figura 7 vemos dibujados estos dos segmentos.
Entonces podríamos colocar el pedestal con la espada en cualquier punto de uno de los segmentos.
En primer lugar elegiremos uno de los dos segmentos, supongamos el segmento B (figura 8) formado por los empty GameObjects B1 y B2.
Luego tomaremos un punto aleatorio entre los dos empty GameObjects, figura 9.
Finalmente en ese punto elegido colocaremos el pedestal.
En el caso de las piezas del pasillo y callejón sin salida que solo tienen una dirección, haremos coincidir los puntos A y B, de esa forma tendremos dos segmentos coincidentes, entonces podremos utilizar la misma solución que para las demás piezas.
Implementación de la estrategia
Hemos elaborado un plan para colocar el pedestal en algún punto interior de alguna de las piezas del laberinto. Ahora en base a esto vamos a resolver el problema.
Primero en la jerarquía voy a separar las piezas obstrucción de las demás, porque estas piezas no las vamos a considerar en nuestra solución.
En la figura 1 vemos seleccionadas las piezas que vamos a utilizar.
Necesitamos encontrar las referencias de estas piezas en nuestro código para poder elegir una de ellas. La forma más simple de hacerlo es utilizar un Tag.
Voy a crear un Tag llamado «SpawnPiece» y se lo voy a asignar a todas las piezas seleccionadas en la figura 12.
El video 2 de la Serie Fundamental de Unity nos muestra varias formas en las que podemos encontrar las referencias de los GameObjects de la jerarquía desde un Script, aquí está el artículo correspondiente a ese video.
A continuación creamos el Script «LabyrinthPiece» (pieza de laberinto) que estará asignado a todas las piezas seleccionadas en la figura 12.
En el Script primero vamos a definir cuatro GameObjects que serán los puntos A1, A2, B1 y B2. Los declaramos como campos serializables para que aparezcan en el inspector y podamos asignarlos manualmente.
Seleccionamos cualquier pieza tipo Encrucijada y le asignamos el Script LabyrinthPiece. En la figura 19 vemos que en el inspector aparecen los campos para los GameObjects.
A continuación vamos a crear los cuatro empty GameObjects que llamaremos A1, A2, B1 y B2. En la figura 20 vemos creado el primer punto. Observen que está definido como hijo de un Empty GameObject llamado Spawn, que a su vez es hijo de la pieza encrucijada.
Vamos a posicionar estos cuatro objetos de acuerdo a la figura 6, en los extremos de los segmentos imaginarios que representan el area caminable dentro de la pieza.
Asignamos estos objetos a sus respectivos campos en el inspector, dentro del componente LabyrinthPiece.
Finalmente aplicamos los cambios. Esto es muy importante porque los cambios los estamos aplicando sobre el Prefab de la encrucijada, es decir que todas las encrucijadas del laberinto ahora van a tener sus propios objetos A1, A2, B1 y B2 y tendrán asignado el componente LabyrinthPiece, con sus propios puntos cargados en los campos.
Podemos comprobar eso chequeando cada encrucijada en la jerarquía y comprobando que tiene estos puntos y el Script asignado.
Lo que sigue es repetir el proceso para las demás piezas. En la figura 26 vemos la pieza en forma de T, este caso es similar a la bifurcación solo que uno de los segmentos imaginarios tendrá su extremo en el centro de la pieza.
En la pieza del pasillo creamos solo los puntos A1 y A2. En la figura 28 vemos que estos puntos también los asignamos en los campos B1 y B2 respectivamente.
En la pieza de la esquina, figura 29, los segmentos imaginarios tendrán dos puntos coincidentes, podríamos crear solo tres Empty GameObjects y uno de ellos asignarlo por ejemplo a A2 y a B1, pero optamos por crear los cuatro puntos.
El caso de la pieza callejón sin salida es igual al del pasillo solo que con menor distancia.
En la figura 31 vemos que en los puntos B1 y B2 repetimos los puntos A.
Método para elegir posición aleatoria de la pieza – Random.Range
En el Script LabyrinthPiece vamos a crear un método público que devolverá un Vector3 que indicará una posición aleatoria de la pieza.
Video sobre métodos – Artículo sobre métodos
La primera instrucción será declarar un Vector3 llamado posición que será el que retornemos al final de la ejecución.
Recordemos que son dos segmentos imaginarios formados uno por los puntos A1 y A2, otro por los puntos B1 y B2. Así que luego vamos a hacer un if para elegir un segmento o el otro.
En el argumento del if usamos Random.Value para generar un número aleatorio entre 0-1 y comprobamos si este valor es menor a 0.5f. Esto quiere decir que tendremos un 50% de probabilidades de elegir el segmento A y otro 50% de elegir el B.
Para elegir un punto aleatorio del segmento imaginario formado por los puntos utilizamos el método Vector3.Lerp, el cual hará una interpolación lineal entre dos Vector3 que indiquemos.
El método recibe tres argumentos, los dos primeros son los Vector3 entre los cuales se interpolará y el tercer parámetro es el punto de interpolación que nos interesa.
Para ejemplificar la función de interpolación consideremos lo siguiente: si el tercer valor del método Lerp vale 0 tendremos un Vector3 igual al primer parámetro indicado. Si vale 1 tendremos un Vector3 igual al segundo parámetro indicado. Y si vale 0.5f tendremos un Vector3 que estará situado exactemente en el punto central entre los dos Vector3 que indicamos como parámetros.
De esta forma usamos Random.Range para generar un Vector3 que se encontrará en algúna posición entre los puntos indicados en los dos primeros parámetros del método Lerp y ese vector lo asignamos al Vector3 posicion que habíamos definido al principio.
En una región del if utilizamos la posición de los puntos A1 y A2. En la otra región del if hacemos exactemente lo mismo pero con los puntos B1 y B2.
Finalmente devolvemos el Vector3 position.
Todo esto que se explicó está resumido en las 7 líneas del método GetRandomPosition en la figura 32.
Ahora bien, esto que hicimos era para el Script LabyrinthPiece que está asignado a cada pieza del laberinto.
En el Script GameControl vamos a crear un método que se encargará de colocar al pedestal en una posición aleatoria del laberinto y hará uso del método público de LabyrinthPiece.
Comenzamos definiendo los campos que se observan en la figura 33 debajo del comentario «//Video 9». Estos son los campo y variables que usaremos para resolver el problema.
En la componente GameControl en el inspector (asignada al GameObject Control), rellenaremos los campos. En SpawnPieceTag escribiremos «SpawnPiece».
En ObjectToFind asignaremos el Prefab del pedestal con la espada, el cual es el objeto a encontrar.
En la distancia mínima por el momento escribimos el valor 75. Luego veremos para qué se utiliza esta variable.
No es necesario que el array LabyrinthPieces aparezca en el inspector así que voy a remover el [SerializeField] seleccionado en la figura 37.
En el método StartGame vamos a encontrar todos los GameObjects de la jerarquía que tengan el Tag que indicamos en el inspector. Última instrucción del método startGame en la figura 40.
Luego declaramos el método PlaceObjectToFind (figura 41) y llamamos este método desde el método StartGame (figura 42).
Método PlaceObjectToFind
Lo que haremos con este método será elegir una pieza aleatoria del laberinto, asegurarnos que esa pieza está lo suficientemente lejos del jugador utilizando la variable «minDistance» que le asignamos 70 en el inspector. Si la pieza seleccionada no cumple este requisito volveremos a elegir otra pieza. De esto se encarga el bucle While que se observa en la figura 43.
Una vez que damos con una pieza que cumple los requisitos, colocaremos el pedestal en un punto aleatorio de su interior. Para ello usamos una versión del método Instantiate, que recibe tres parámetros: El primero es el objeto a encontrar almacenado en el campo «objectToFind», el segundo es la posición que la recibiremos automáticamente de la pieza del laberinto ejecutando el método GetRandomPosition de la componente LabyrinthPiece que tiene asignada. El tercer parámetro es la rotación, aquí indicaremos: Quaternion.identity (una rotación identidad).
Es importante que guardemos la referencia de este nuevo objeto que hemos creado, lo haremos en «objectToFindInstance» (última instrucción en la figura 43). De esta forma cuando la partida se acabe podremos destruir este objeto manualmente.
En el método EndGame hacemos la destrucción de la instancia del objeto a encontrar, figura 44.
Al entrar en el modo juego y apretar el botón Start, todo parece funcionar correctamente. El pedestal con la espada aparece en una posición aleatoria del laberinto, dentro de una de las piezas.
Colocar objeto fuera de las piezas
Hay regiones del laberinto que se encuentran fuera de las piezas, por ejemplo la que se encuentra resaltada en la figura 47. Podríamos estar interesados en colocar el objeto en una posición perteneciente a esta área.
¿Cómo podríamos reutilizar lo que hemos hecho?
Para empezar creamos un Empty GameObject y lo llamamos SpawnArea y lo ubicamos entre las piezas del laberinto.
Luego creamos cuatro empty GameObjects para representar los puntos A1, A2, B1 y B2. Colocamos estos objetos en los extremos de los dos segmentos imaginarios del area resaltada en verde en la figura 49.
Luego creamos un Prefab junto a las demás piezas del laberinto (figura 51), porque puede que reutilicemos este objeto cambiando de lugar los puntos internos.
Luego le asignamos la componente LabyrinthPiece y colocamos los puntos internos en los campos respectivos.
No debemos olvidar asignar el Tag SpawnPiece en el inspector, de otro forma estas áreas no serán consideradas a la hora de elegir una posición para el pedestal. En mi caso, como se observa en la figura 53, no lo había asignado y estuve varios minutos probando para que el pedestal apareciera en estas áreas.
Detalles
Al probar el juego noté que la puerta tenía un tamaño bastante grande en relación al personaje, así que la hice un poco más pequeña y apliqué los cambios.
Otro problema que detecté es que había piezas con el Tag SpawnPiece cuya región interior resultaba inaccesible para el jugador. En la figura 55 observa una de estas piezas, si el pedestal aparece aquí, el jugador no podrá encontrarlo.
La solución a esto es seleccionar esta pieza y quitarle el Tag SpawnPiece, de esta forma la pieza no será considerada.
Conclusión
En este artículo logramos colocar el pedestal en una posición aleatoria dentro del laberinto.
Para hacerlo tuvimos que analizar el tipo de piezas que forman el laberinto, establecer ciertas reglas y plantear una estrategia que resuelva nuestro problema.
Hicimos uso del pensamiento de programación orientada a objetos para crear una solución flexible que se adapte a todos los tipos de piezas.
Como usualmente ocurre, no hay un solo camino para resolver un problema. Otra forma de abordar esta situación es hacer un Bake de un Navmesh y tomar un punto dentro de estas regiones.