#9 Pedestal en posición aleatoria. Random.Range

Por GameDevTraum

Introducción

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

En el siguiente video puedes ver cómo resuelvo este problema.

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.

pedestal con espada instanciado aleatoriamente en un laberinto utilizando random.range unity
Fig. 1: El pedestal con la espada aparece en una posición aleatoria del escenario.
miniatura video 4 del juego del laberinto, colocar personaje aleatoriamente en escenario, unity
Fig. 2: En el video y artículo 4 colocamos al personaje aleatoriamente en una de las puertas.

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.

piezas simples que forman un laberinto para un prototipo en unity
Fig. 3: Estas son las piezas del laberinto en las que se puede caminar.

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.

piezas simples que forman un laberinto para un prototipo en unity
Fig. 4: En estas regiones podremos colocar el pedestal.

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.

encrucijada de laberinto con texturas
Fig. 5: Pieza encrucijada del laberinto.

encrucijada de laberinto con texturas, puntos extremos marcados
Fig. 6: En los extremos de la pieza colocamos 4 puntos.

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.

encrucijada de laberinto con texturas, puntos extremos forman dos segmentos
Fig. 7: Estos puntos forman dos segmentos, horizontal y vertical.

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.

encrucijada de laberinto con texturas, uno de los segmentos se eligen al azar con random.range
Fig. 8: Supongamos que elegimos el segmento B.

encrucijada de laberinto con texturas, se elige un punto aleatorio del segmento con random.range
Fig. 9: Elegimos un punto aleatorio del segmento B.

Finalmente en ese punto elegido colocaremos el pedestal.

encrucijada de laberinto con texturas, posicion final donde se coloca el pedestal utilizando Random.Range
Fig. 10: En el punto elegido colocaremos el pedestal con la espada.

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.

pasillo de laberinto con texturas, tomaremos un punto aleatorio del segmento con random.range
Fig. 11: En el caso del pasillo haremos coincidir los segmentos A y B.

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.

prototipo de laberinto hecho en unity
Fig. 12: Seleccionamos el laberinto y separamos las piezas que obstruyen

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.

crear tag en unity, spawnpiece para las piezas en las que podremos colocar el objeto de manera aleatoria usando random.range
Fig. 13: Creamos el tag SpawnPiece para las piezas que podrán contener al laberinto.

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.

se seleccionan todas las piezas que pueden contener al pedestal
Fig. 14: Seleccionamos todas las piezas que podrán contener al pedestal.

asignacion de tag a uno o mas gameobjects en unity
Fig. 15: A las piezas seleccionadas se les asigna el tag SpawnPiece.

A continuación creamos el Script “LabyrinthPiece” (pieza de laberinto) que estará asignado a todas las piezas seleccionadas en la figura 12.

creacion de script en unity que se encargara de colocar un objeto en una posicion aleatoria del laberinto usando random.range
Fig. 16: Creamos un nuevo Script llamado LabyrinthPiece, que asignaremos a las piezas que puedan contener al pedestal.

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.

definicion de campos serializados en script c sharp, unity
Fig. 17: Definimos cuatro campos tipo GameObject para contener a los puntos de cada pieza.

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.

prototipo de laberinto hecho en unity
Fig. 18: En la jerarquía seleccionamos la encrucijada.

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity
Fig. 19: Asignamos el Script LabyrinthPiece a la encrucijada.

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.

encrucijada de laberinto con texturas
Fig. 20: Creamos cuatro empty GameObjects como hijos de esta pieza.

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.

encrucijada de laberinto con texturas
Fig. 21: Utilizando la perspectiva ortográfica, posicionamos los empty GameObjects, uno en cada extremo.

encrucijada de laberinto con texturas
Fig. 22: Utilizando la perspectiva ortográfica, posicionamos los empty GameObjects, uno en cada extremo.

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.

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity
Fig. 23: Asignamos los empty GameObjects a los espacios en el inspector.

aplicar cambios en inspector para extender la configuracion a los demas prefabricados
Fig. 24: Aplicamos los cambios para que todas las encrucijadas del escenario tengan esta misma configuración.

Podemos comprobar eso chequeando cada encrucijada en la jerarquía y comprobando que tiene estos puntos y el Script asignado.

ventana jerarquia en unity, gameobjects que representas piezas de un laberinto
Fig. 25: Al aplicar los cambios, todas las encrucijadas del laberinto pasan a tener los objetos vacíos y el Script LabyrinthPiece.

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.

prototipo de laberinto hecho en unity, piezas de laberinto en la jerarquia
Fig. 26: Empty objects para la pieza bifurcación.

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.

prototipo de laberinto hecho en unity, piezas de laberinto en la jerarquia
Fig. 27: Empty objects para la pieza tipo pasillo.

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity
Fig. 28: Para la pieza del pasillo asignamos los puntos A1 y A2 también a los campos B1 y B2.

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.

prototipo de laberinto hecho en unity, piezas de laberinto en la jerarquia
Fig. 29: Empty objects para la pieza tipo esquina.

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.

prototipo de laberinto hecho en unity, piezas de laberinto en la jerarquia
Fig. 30: Empty objects para la pieza callejón sin salida

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity
Fig. 31: Para la pieza callejón sin salida asignamos los puntos A1 y A2 también a los campos B1 y B2.

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étodosArtí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.

metodo publico en script c sharp para colocar objeto en posicion aleatoria usando Random.Range
Fig. 32: El método GetRandomPosition entregará una posición aleatoria dentro de la pieza.

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.

definicion de campos serializados en script c sharp, unity
Fig. 33: En el Script GameControl definimos un array de GameObjects para las piezas del laberinto.

En la componente GameControl en el inspector (asignada al GameObject Control), rellenaremos los campos. En SpawnPieceTag escribiremos “SpawnPiece”.

ventana inspector de un gameobject, el campo spawn piece tag pertenece a todas las piezas en las que podremos colocar el objeto de manera aleatoria usando Random.Range
Fig. 34: Escribimos el nombre del tag que asignamos en las piezas del laberinto, en este caso “SpawnPiece”

En ObjectToFind asignaremos el Prefab del pedestal con la espada, el cual es el objeto a encontrar.

prefabs en unity, pedestal con espada y reloj de pendulo, estos objetos seran colocados en una posicion aleatoria del laberinto usando Random.Range
Fig. 35: Buscamos el prefab del pedestal con la espada que hicimos en el video 2 de la serie.

En la distancia mínima por el momento escribimos el valor 75. Luego veremos para qué se utiliza esta variable.

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity
Fig. 36: Asignamos el Prefab del pedestal al campo ObjectToFind.

No es necesario que el array LabyrinthPieces aparezca en el inspector así que voy a remover el [SerializeField] seleccionado en la figura 37.

campos serializados, privados y publicos en un script c sharp para un prototipo de juego de laberinto en unity
Fig. 37: No es necesario que el array de GameObject de las piezas del laberinto sea visible en el inspector.
campos serializados, privados y publicos en un script c sharp para un prototipo de juego de laberinto en unity
Fig. 38: No es necesario que el array de GameObject de las piezas del laberinto sea visible en el inspector.
ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity
Fig. 39: Al remover la línea SerializeField, el campo no aparece en el inspector ya que es privado.

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.

metodo start de script c sharp en unity para un prototipo de juego de laberinto
Fig. 40: En el método Start de GameControl encontramos todos los GameObjects con el Tag indicado.

Luego declaramos el método PlaceObjectToFind (figura 41) y llamamos este método desde el método StartGame (figura 42).

este metodo se encargara de colocar un objeto en una posicion aleatoria del laberinto usando Random.Range
Fig. 41: Definimos un método privado que se encargará de colocar el pedestal en alguna pieza del laberinto.

metodo start game de script c sharp en unity para un prototipo de juego de laberinto
Fig. 42: Hacemos la llamada desde el método StartGame, es decir, cuando el juego empieza el pedestal es colocado en el escenario.

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

este metodo se encargara de colocar un objeto en una posicion aleatoria del laberinto usando Random.Range
Fig. 43: Instrucciones del método PlaceObjectToFind.

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.

la instancia del objeto que fue colocado en una posicion aleatoria usando Random.Range
Fig. 44: En el método EndGame se destruye la instancia del pedestal.

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.

prototipo de laberinto hecho en unity, pedestal con espada. el objeto es colocado en una posicion aleatoria del laberinto usando Random.Range
Fig. 45: Al entrar en el modo juego el pedestal es colocado en una de las piezas del escenario.

prototipo de laberinto hecho en unity, el objeto es colocado en una posicion aleatoria del laberinto usando Random.Range
Fig. 46: Podemos ver desde la ventana editor de qué pieza se trata.

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?

prototipo de laberinto hecho en unity. en una posicion aleatoria de estas areas colocaremos el objeto usando Random.Range
Fig. 47: Esta región puede ser un lugar donde quisiéramos que aparezca el pedestal.

Para empezar creamos un Empty GameObject y lo llamamos SpawnArea y lo ubicamos entre las piezas del laberinto.

jerarquia de un prototipo de juego de laberinto hecho en unity
Fig. 48: Creamos un Empty GameObject y lo llamamos SpawnArea, esto nos permitirá colocar el pedestal en otras partes del laberinto.

jerarquia de un prototipo de juego de laberinto hecho en unity
Fig. 49: Los cuatro puntos de la región son hijos del GameObject SpawnArea.

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.

prototipo de laberinto hecho en unity. en una posicion aleatoria de estas areas colocaremos el objeto usando Random.Range
Fig. 50: Vamos a crear 4 empty GameObjects para utilizar en el script LabyrinthPiece.

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.

prefabs en unity, piezas para crear un laberinto
Fig. 51: Tomamos el GameObject SpawnArea y creamos un Prefab para reutilizar.

Luego le asignamos la componente LabyrinthPiece y colocamos los puntos internos en los campos respectivos.

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity
Fig. 52: Asignamos el Script LabyrinthPiece al GameObject SpawnArea.

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.

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity, asignar tags
Fig. 53: Debemos recordar asignar el tag SpawnPiece al GameObject SpawnArea. Aplicar cambios.

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.

prototipo de laberinto hecho en unity, puertas de entrada-salida
Fig. 54: Las puertas estaban demasiado grandes en relación al personaje.

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.

prototipo de laberinto hecho en unity
Fig. 55: Esta pieza se encuentra fuera del alcance del personaje, el pedestal no tiene que aparecer aquí.

ventana inspector de un gameobject perteneciente a un prototipo de laberinto hecho en unity, asignar tags
Fig. 56: Seleccionamos esa pieza y en el campo tag seleccionamos: “Untagged”. No aplicamos los cambios.

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.