Unity Prefabs

De MediaWiki
Ir a la navegación Ir a la búsqueda

Introducción


  • Un Prefab es un concepto clave y un recurso fundamental en el desarrollo de juegos en Unity.


  • Podemos definir un Prefab como un Asset al que hemos añadido un transform y cualquier otro componente.
Fijarse que digo Asset y no GameObject.
Recordar que cuando vimos la diferencia entre Asset y GameObject indicamos que este último tiene un transform asocioado, y un Asset no.
  • Sin embargo, un Prefab es un Asset con un trasnform asociado (y otros componentes) pero a diferencia del GameObject, no se encuentra en la escena inicialmente, sino que dinámicamente (es decir, durante la ejecución del juego) se van creando nuevos GameObjects basados en el Prefab, el cual tiene un transform y otros componentes asociados.


  • Un prefab nos sirve para 'plantilla' para crear nuevos gameobjects en la escena, con unos componentes ya asociados.
Imaginar que disponemos de un Prefab que muestra una bala. Dicho Prefab tiene asociado un script que hace que la bala vaya hacia adelante a una velocidad.
Cuando pulsamos la tecla de disparo, crearíamos un gameobject basado en este modelo, y al crearlo, se ejecutaría su script asociado y la bala empezaría a desplazarse hacia adelante.
De esta forma no tenemos que tener todas las balas en forma de GameObjects ya creados, en la escena.


  • Enemigos que 'aparecen' cada cierto tiempo es otro ejemplo de uso de los prefabs.


  • Preparación previa:
  • Para realizar los ejercicios de esta sección, crea una nueva escena de nombre Escena_Prefabs.



Crear Prefabs

  • Preparación previa:
  • Crea un 'Empty GameObject' de nombre 'CrearPrefab' situado en la posición (0,0,0) (recuerda que puedes hacer un 'reset' del componente Transform pulsando sobre la rueda dentanda a la derecha del componente)
  • A partir de ahora, crea cualquier GameObject que se indique como 'hijo' de este GameObject vacío.


  • Para crear un Prefab, lo único que tenemos que hacer es arrastrar un GameObject a la ventana Project Window.
Normalmente, para tener todo un poco organizado, se crea una carpeta de nombre Prefabs y dentro de la misma se organiza según el juego, como vimos en este punto de la Wiki.


  • Ya está !!!
Es fácil eh :)
  • Fijarse, como he comentado al principio que ahora disponemos de un 'Asset' especial, ya que este asset está formado por todos los componentes que tenía el gameobject al que hemos asociado.
Ahora con este prefab vamos a poder 'crear' múltiples gameobjects con los mismos componentes.
Arrastrar ahora dos veces el prefab hasta el Empty GameObject de la escena.
Unity3d prefabs 3.jpg
  • Se crean dos nuevos GameObjects con los mismos componentes que los que hay en el Prefab. Moverlos para que no estén en la misma posición...


  • Al estar en 'azul' significa que todos ellos están conectados al prefab.
Esto quiere decir que cualquier modificación sobre el Prefab o sobre los GameObjects que se encuentren en azul será replicada a todos ellos.
  • En el ejemplo, vamos a añadir al Prefab un componente Collider, concretamente un Cápsule Collider.


  • Esta conexión es bidireccional, quiere decir que si modifico el GameObject también puedo modificar el Prefab asociado y al actualizar el prefab también se actualizarían todos los GameObjects asociados.
Vamos a crear una carpeta en la Project Window (si no lo está ya) de nombre /Assets/Scripts/UD3/Prefabs como muestra la imagen:
Unity3d prefabs 6.jpg
Dentro de dicha carpeta vamos a crear un script de nombre 'ProyectilGestion' que inicialmente va a hacer que el GameObject que tenga asociado este script, se mueva hacia adelante a una velocidad indicada en la ventana Inspector.
 1 using UnityEngine;
 2 
 3 public class ProyectilGestion : MonoBehaviour {
 4 
 5     [SerializeField]
 6     private float velocidad;
 7 
 8     private void Reset()
 9     {
10         velocidad = 5f;
11     }
12     // Use this for initialization
13     void Start () {
14 		
15 	}
16 	
17 	// Update is called once per frame
18 	void Update () {
19 
20         transform.Translate(transform.forward * velocidad * Time.deltaTime);
21 
22 
23 	}
24 }


  • Una vez creado, vamos a añadir este script al gameobject de nombre Projectil(1).
Si ejecutamos el juego podemos ver como los tres proyectiles van hacia adelante.



  • Si queremos seleccionar automáticamente el Prefab asociado a un GameObject, pulsaremos el botón 'Select' del GameObject en la ventana Inspector:



  • Si estamos trabajando con un GameObject asociado a un Prefab y 'nos equivocamos al realizar cualquier operación sobre el mismo' podemos 'recuperar' los componentes originales del Prefab asociado pulsando el botón Revert.



Rompiendo la relación entre Prefab-GameObject

  • En cualquier momento podemos hacer que un GameObject deje de tener relación con el Prefab y pase a ser un GameObject como los creados hasta ahora.
Cualquier modificación que se haga sobre el mismo no afectará ni al Prefab ni al resto de GameObjects basados en el Prefab.



Creando Prefabs en ejecución

  • La gran ventaja que tiene el uso de Prefabs es que vamos a poder crear (en programación orientada a objetos decimos instanciar) nuevos GameObjects mientras se ejecuta el juego.
  • Ejemplos de uso de esto podrían ser:
  • Disparos por parte de cualquier enemigo o del protagonista.
  • Crear nuevos enemigos.






Referenciando a otros GameObjects/Components de la escena desde un Prefab

  • Preparación previa:
  • Deshabilita el GameObject 'CrearPrefab'.
  • Crea un Empty GameObject y cambia el nombre por el de 'ReferenciarGameObject'
  • Crea un Proyectil basado en una Cápsula como en el ejemplo anterior.
  • Crea un Prefabs basado en el anterior GameObject de nombre 'ProyectilConSeguimiento'.
  • Añade a la escena una esfera de nombre 'Objetivo' y sepárala del proyectil.
Unity3d prefabs 17.jpg


  • Dentro de un Prefab no podemos tener referencias a GameObjects o componentes concretos de un GameObject de la escena.
Vamos a crear un Script de nombre ProyectilConSeguimientoGestion:
El movimiento de un GameObject ya lo hemos visto anteriormente en esta Wiki.
 1 using UnityEngine;
 2 
 3 public class ProyectilConSeguimientoGestion : MonoBehaviour {
 4 
 5     [SerializeField]
 6     private Transform objetivo;
 7     [SerializeField]
 8     private float velocidad;
 9 
10     private void Reset()
11     {
12         velocidad = 5f;
13     }
14 
15     // Use this for initialization
16     void Start () {
17 		
18 	}
19 	
20 	// Update is called once per frame
21 	void Update () {
22 
23         transform.position = Vector3.MoveTowards(transform.position, objetivo.position, velocidad * Time.deltaTime);
24 	}
25 }


  • Si asociamos este script al Prefab 'ProyectilConSeguimiento' podemos comprobar como no nos deja 'asociar' el transform de la esfera a la propiedad Objetivo:
Unity3d prefabs 18.jpg
Lo mismo pasaría si ponemos como propiedad un GameObject...
Para solucionarlo, tendremos que emplear alguna de las técnicas vistas en el punto de la wiki Obtener una referencia a cualquier GameObject.
En este ejemplo vamos a emplear la técnica de establecer un Tag sobre la esfera.
Cambiamos el Tag de la esfera por el de Player.
Unity3d prefabs 19.jpg


  • Modificamos el script:
 1 using UnityEngine;
 2 
 3 public class ProyectilConSeguimientoGestion : MonoBehaviour {
 4 
 5     [SerializeField]
 6     private float velocidad;
 7 
 8     private Transform objetivo;
 9 
10     private void Reset()
11     {
12         velocidad = 5f;
13     }
14 
15     // Use this for initialization
16     void Start () {
17         objetivo = GameObject.FindGameObjectWithTag("Player").transform;		
18 	}
19 	
20 	// Update is called once per frame
21 	void Update () {
22         transform.position = Vector3.MoveTowards(transform.position, objetivo.position, velocidad * Time.deltaTime);
23 	}
24 }
  • Línea 8: Cambiamos la propiedad a Private ya que no la vamos a asignar desde Unity gráficamente.
  • Línea 17: Buscamos el GameObject con el tag 'Player' y asignamos el transform a la propiedad objetivo.



Creando Prefabs dinamicamente

  • Más información en:


  • Una de las principales ventajas que tiene el uso de Prefabs es que vamos a poder crear nuevos GameObjects basados en un Prefab de forma dinámica mientras se está ejecutando el juego.
Ejemplo de uso:
  • Disparos
  • Aparición de nuevos enemigos.
  • Crear laberintos de forma dinámica.



Vamos a ver un caso práctico.
Descargar de la Asset Store un modelo de un coche, por ejemplo este: https://assetstore.unity.com/packages/3d/vehicles/land/car-20128
Una vez importado a Unity, crea una nueva escena o un Empty GameObject deshabilitando los demás, de nombre PrefabsDinamicos
Dentro de la escena añade una esfera a la posición (0,0,0) y de escala (4,4,4)
Ahora lo que vamos a hacer es crear un GameObject a partir del modelo importado al que vamos a asociar un script que hará que se mueva hacia adelante a una velocidad indicada en la ventana Inspector.
El código del script es el siguiente:
 1 using UnityEngine;
 2 
 3 public class MoverVehiculo : MonoBehaviour {
 4 
 5     [SerializeField]
 6     private float velocidad;
 7 
 8     private void Reset()
 9     {
10         velocidad = 5;
11     }
12 
13 	// Update is called once per frame
14 	void Update () {
15         transform.Translate(Vector3.forward * velocidad * Time.deltaTime, Space.Self);
16 	}
17 }
  • El proceso sería:


  • En este punto ya tenemos nuestro Prefab preparado para ser 'instanciado'.
Vamos a hacer que cuando se presione la tecla espaciador, se cree un nuevo vehículo en la posición de la esfera.
Para ello vamos a crear un Empty GameObject de nombre SpawnVehiculos, que será el punto del cual partirán los vehículos creados.
Podríamos emplear la esfera, pero de esta forma podremos mover el punto de salida sin mover la esfera.
El script que vamos a asociar a este Empty GameObject será el siguiente:
 1 using UnityEngine;
 2 
 3 public class SpawnVehiculos : MonoBehaviour {
 4 
 5     [SerializeField]
 6     private GameObject cocheBase;
 7 
 8 	// Use this for initialization
 9 	void Start () {
10 		
11 	}
12 	
13 	// Update is called once per frame
14 	void Update () {
15 
16         if (Input.GetKeyDown(KeyCode.Space))
17         {
18             GameObject coche = Instantiate(cocheBase, transform.position,transform.rotation);
19         }
20 		
21 	}
22 }
  • Línea 19: Fijarse como lleva como argumentos la posición y rotación de un GameObject.
En nuestro ejemplo estamos indicando la posición y rotación del Empty GameObject, pero podríamos indicar la de cualquiera.


  • Como vemos, todos los GameObject se crean en la 'raíz' de la escena.
Si queremos podemos hacer que todos ellos sean hijos de otro GameObject cualquiera.
En nuestro caso, vamos a hacer que sean hijos del Empty GameObject de donde salen. Para ello solo tenemos que modificar la propiedad 'transform.parent' del gameobject creado para que 'apunte' al EmptyGameObject (recordar que el script que crea los coches está asociado al Empty GameObject).
 1 using UnityEngine;
 2 
 3 public class SpawnVehiculos : MonoBehaviour {
 4 
 5     [SerializeField]
 6     private GameObject cocheBase;
 7 
 8 	// Use this for initialization
 9     void Start () {
10 		
11 	}
12 	
13     // Update is called once per frame
14     void Update () {
15 
16         if (Input.GetKeyDown(KeyCode.Space))
17         {
18             GameObject coche = Instantiate(cocheBase, transform.position,transform.rotation);
19             coche.transform.parent = transform;
20         }
21 		
22 	}
23 }


Unity3d prefab din 7.jpg



Estableciendo el tiempo para crear un Prefab

  • Cuando creamos Prefabs dinámicamente vamos a querer que estos se creen automáticamente cada 'cierto tiempo'.
  • Esto lo podemos hacer de dos formas:


  • Podéis comprobar en este enlace un ejemplo de uso de Coroutines como mejor opción a InvokeRepeating.


CoRoutine


  • Permite 'pausar' la ejecución del método que esté definido como corutine.
No crea un hilo de ejecución diferente del principal, por lo que siempre tiene que tener la orden yield return .... para que el hilo principal siga ejecutándose.
  • El proceso para hacer uso de este método es el siguiente:
  • Creamos un procedimiento que va a ser el que vamos a llamar cada X segundos y donde se van a instanciar los GameObjects.
La definición del procedimiento debe hacer uso de la class IEnumerator: private IEnumerator CrearVehiculos(float tiempo) { }
Para ello es necesario importar el espacio de nombres System.Collection: using System.Collections;
Dentro del procedimiento añadimos las órdenes que queramos que se ejecuten.
Podemos tener un bucle infinito pero siempre necesitamos que se ejecuta la orden yield return. Si no queremos que espere nada, podemos poner: yield return null
Esto lo que hace es salir de la corutina y volver a ejecutar la siguiente línea en el siguiente frame.
Para indicar que 'espere' un cierto número de segundos hay que usar la orden: yield return new WaitForSeconds(NumSegundosAEsperar);. Es necesario poner siempre esta orden (yield) en algún punto del procedimiento.
  • Una vez creado el procedimiento se llama en el momento que queramos haciendo uso del método: StartCoroutine(CrearVehiculos(2)); (es un ejemplo).
Debemos de tener cuidado de no realizar la llamada en el método Update() sin ningún tipo de condición.


  • Siguiendo con nuestro ejemplo, vamos a modificar el script para que se creen 5 coches con un intervalo de 2 segundos entre cada coche:
 1 using System.Collections;
 2 using UnityEngine;
 3 
 4 public class SpawnVehiculos : MonoBehaviour {
 5 
 6     [SerializeField]
 7     private GameObject cocheBase;
 8 
 9     private int numCochesCreados;
10 
11     void Start()
12     {
13         Debug.Log(Time.time);
14         numCochesCreados = 0;
15 
16 
17         StartCoroutine(CrearVehiculos(2));
18     }
19     private IEnumerator CrearVehiculos(float tiempo)
20     {
21 
22         while (numCochesCreados<5)
23         {
24             GameObject coche = Instantiate(cocheBase, transform.position, transform.rotation);
25             coche.transform.parent = transform;
26             numCochesCreados++;
27 
28             yield return new WaitForSeconds(tiempo);     // Espera dos segundos para seguir la ejecución
29         }
30 
31     }
32 
33     // Update is called once per frame
34     void Update () {
35 
36 		
37 	}
38 }


En la imagen se puede apreciar que la separación entre los coches es la misma.
Unity3d prefab din 8.jpg


Siguiendo con nuestro ejemplo, vamos a hacer que cuando se pulse la tecla S, se pare de crear coches:
 1 using System.Collections;
 2 using UnityEngine;
 3 
 4 public class SpawnVehiculos : MonoBehaviour {
 5 
 6     [SerializeField]
 7     private GameObject cocheBase;
 8 
 9     private int numCochesCreados;
10     private IEnumerator coroutine;
11 
12     void Start()
13     {
14         Debug.Log(Time.time);
15         numCochesCreados = 0;
16 
17         coroutine = CrearVehiculos(2);
18         StartCoroutine(coroutine);
19     }
20     private IEnumerator CrearVehiculos(float tiempo)
21     {
22 
23         while (numCochesCreados<5)
24         {
25             GameObject coche = Instantiate(cocheBase, transform.position, transform.rotation);
26             coche.transform.parent = transform;
27             numCochesCreados++;
28 
29             yield return new WaitForSeconds(tiempo);     // Espera dos segundos para seguir la ejecución
30         }
31 
32     }
33 
34     // Update is called once per frame
35     void Update () {
36         if (Input.GetKeyDown(KeyCode.S))
37         {
38             StopCoroutine(coroutine);
39         }
40 		
41 	}
42 }
  • Líneas 36-39: Paran la coroutine. Para no tener que hacerlo utilizando el nombre del método con el parámetro, hacemos uso de una propiedad private que guarda la referencia al nombre del método IEnumerator con el parámetro (líneas 10 y 17)



  • Nota: También podemos salir de la coroutine con la orden: yield break;





Enlace a la página principal del curso





-- Ángel D. Fernández González -- (2018).