Unity Animaciones
Sumario
Introducción
- Más información en:
- En Unity podemos gestionar animaciones que vengan ya incorporadas a los modelos importados o bien podemos realizar nuestras 'propias animaciones'.
- Cada una de las animaciones conforma lo que se denomina Clip en Unity.
- Las animaciones propias son animaciones consistentes en modificar alguno de los parámetros que conformar el transform de cada GameObject.
- Es decir podemos modificar:
- Posición
- Escala
- Rotación
- Estas modificaciones se producirán a lo largo de un período de tiempo y que conformará la animación.
- Por ejemplo:
- Podemos tener un soldado que siempre se esté moviendo de izquierda a derecha sin parar y que se desplace 10 metros.
- O un pájaro que vuele por la pantalla siguiendo una ruta 'predefinida' y que se repita continuamente.
- En cuanto a las animaciones ya incorporadas a los modelos, estas animaciones están asociadas a un nombre (por ejemplo, andar, correr,...)
- Unity permite crear una máquina de estados en las que cada animación tiene lugar en función del estado en que se encuentre el GameObject. Este pasará de un estado a otro en función de los valores que puedan tomar variables definidas por nosotros.
Importando modelos con animaciones
- Muchas de las animaciones que vamos a poder utilizar son aquellas en las que el modelo tiene asociado un esqueleto o bones.
- Como vemos en la imagen, la figura 3D tiene asociado un RIG.
- Básicamente consiste en asociar a un modelo 3D un conjunto de huesos los cuales están asociados a la figura, de tal forma que al mover dichos huesos, movemos las partes de la figura 3D asociada.
- Este tipo de animaciones se denominan Humanoid en Unity y están asociados a un Avatar.
- Los modelos que vienen con este tipo de animaciones tienen una pose en T.
- Suelen ser todos bípedos y con unas características comunes para que:
- Los esqueletos y movimientos asociados puedan ser aplicados a cualquier figura 3D con esta forma (retargeting)
- Permite realizar Inverse Kinematic, es decir, indicar un punto determinado del espacio y hacer que los huesos del esqueleto se muevan correctamente para llegar a ese punto.
- Podemos disponer de modelos 3D que no se adaptan a este tipo y que Unity denomina Generic:
- En la imagen tenemos una figura 3D que no responde al tipo de 'Humanoide'.
- Uno de los sitios que actualmente es gratuito y permite 'asociar' animaciones a modelos 3D es https://www.mixamo.com.
- Una vez registrados accederemos a su página que actualmente tiene este aspecto:
- Indicar que tanto en animaciones como en modelos, podemos filtrar los resultados buscando por palabras claves.
- Si nos vale alguno de los modelos, veremos a continuación como hacer uso de ellos en Unity.
- En el ejemplo siguiente partimos que no nos vale ninguno de los modelos y que utilizamos otro descargado o diseñado por nosotros mismos.
- Recordar que esto solo vale para animaciones de tipo 'humanoide' como ya explicamos anteriormente.
- Actualmente se puede descargar la aplicación Fuse de Adobe que permite diseñar nuestros avatares de forma totalmente personalizable.
Vamos a alguna página de descarga de modelos 3D. En el ejemplo es https://www.turbosquid.com. Al elegir el modelo mejor es que tenga un rigged asociado. En caso contrario, Mixamo puede intentar crear uno, pero algunas veces no funciona. Depende del modelo.
Una vez tengamos la animación como nos gusta, descargamos la figura con la animación. Lo que vamos a hacer es descargar varias animaciones, pero como el modelo es siempre el mismo, solo descargaremos la información del esqueleto en uno de los archivo. Esta primera animación la descargamos con el modelo (with Skin).
Con el Zombie Idle seleccionado, podemos ir a la sección Animation de la ventana Inspector. En ella podemos ver que dicho modelo tiene asociado un Clip que es la animación descargada. Podemos ver como es dicha animación en la parte baja de la ventana Inspector. Puede suceder que un modelo tenga guardadas varias animaciones y por tanto aparecería una lista de Clips. También es posible desde Unity 'trocear' un Clip en varias partes.
Ahora seleccionamos otro de los modelos, por ejemplo 'Zombie Atack'. Si recordáis, cuando descargamos el modelo de Mixamo escogimos la opción Without Skin. Es por eso que ahora tenemos que informar a Unity que este modelo va a hacer uso de los datos del modelo 'Zombie Idle'. Recordar pulsar el botón Apply.
- Indicar que los nombres de los Clips cuando lo descargamos de la web de Mixamo son siembre mixamo.com.
- Aunque en el ejemplo que viene a continuación no están cambiados, para realizar operaciones posteriores va a ser necesario que tengan un nombre significativo.
- Para cambiar el nombre del Clip, debemos de seleccionar el Modelo en la ventana Project y sobre la ventana Inspector => Animation, cambiar el nombre:
Gestionando las animaciones. Animator Controller
Creando un Animator Controller. Animación inicial
- En este punto ya disponemos de 4 modelos con 4 animaciones (Clips).
- Fijarse que solo en una de ellas se previsualiza el modelo.
- Si desplegáis el contenido de cada modelo (presionando la flecha hacia la derecha en cada uno) podéis ver como en todos ellos disponemos de un 'Clip' que viene a ser la animación asociada al mismo.
- Ahora es necesario gestionar dichas animaciones. Debemos de tener algún mecanismo que nos permite controlar cuando la figura 3D está en una animación o en otra.
- Cuando pulsa una tecla quiero que pase de Idle a Walk.
- Cuando presiono el botón de disparo quiero un Attack.
- Todo esto se controla con una máquina de estado a a través de un Animator Controller de Unity.
- Al pulsar dos veces sobre el AnimationController (darle un nombre adecuado), accedemos a la máquina de estados:
Como vemos aparece una serie de 'cajas' que indican estados. Por defecto tenemos tres estados: entry que indica el estado inicial cuando la figura 3D aparece en la escena, any state que representa cualquier estado y exit cuando queremos indicar que debe salir de la máquina de estados, pero lo que provoca es que vuelva al estado Entry. En la figura, estamos arrastrando la animación del Zombie Idle al AnimationController. Al hacerlo se crea un nuevo estado y aparece una flecha que va desde el estado 'Entry' al estado 'mixamo_com' que es la animación del zombie estando idle.
Reutilizando las animaciones sobre otros modelos
- Imaginemos que estas mismas animaciones las queremos utilizar con otros 'modelos', en este caso, de tipo 'Humanoide'.
Si cuando vayáis a la sección Animation o Material, la figura no aparece con las texturas, será que aún no arreglaron el bug del programa, por lo que tendréis que extraer las texturas y el material asociado. Os pedirá una carpeta. Dejar la que abre por defecto. Seguramente os saldrá una ventana indicando que una textura debe ser usada como 'Normal Map'. Presionar el botón Fix Now.
Arrastramos el modelo a la escena, creando un nuevo GameObject. Como se puede ver la ZombieGirl es muy grande. Vamos a modificar el factor de escala. Recuerda que ya vimos en esta Wiki como ajustar la escala de los modelos importados.
Analizando la máquina de estados
- Más información:
- Como ya comentamos anteriormente el Animator Controller permite tener una máquina de estados. En cada estado vamos a poder 'asociar' una animación y vamos a poder pasar de un estado a otro en base a diferentes tipos de condiciones.
- Esas condiciones las vamos a poder modificar por medio de scripts.
- Imaginemos un caso típico en el que en base a la vida que le queda al zombie, este se muere.
- Para ello vamos a necesitar un parámetro vida o bien una variable de tipo boolean que indique cuando el zombie ha muerto.
- Vamos a optar por la variable de tipo boolean.
- Podríamos optar por el parámetro de vida si por ejemplo, al tener diferentes valores, quisiéramos diferentes animaciones. Imaginar que en caso de que le quede 5 de vida, pasaríamos a que el zombie se arrastrara. Y en caso de llegar a 0 moriría.
- Estando en el Animator Controller del Zombie podemos ver los tipos de parámetros que podemos crear:
- Float: Una variable de tipo Flotante.
- Int: Una variable de tipo Entero.
- Bool: Una variable de tipo Booleana.
- Trigger: Viene a ser una variable de tipo Booleana (admite valores true/false) con la diferencia que una vez la transición se ha producido su valor vuelve a valer false automáticamente.
- Un ejemplo de uso de este tipo de variables puede ser cuando estamos disparando un arma y se acaba la munición. En ese momento debemos de pasar a la animación de cargar el arma (sería poniendo a true una variable de este tipo) e inmediatamente después continuaríamos con la animación de disparar. Al pasar a la animación de disparar, la variable Trigger ya valdría false otra vez.
- Veamos un ejemplo práctico.
Ahora debemos de plantearnos ¿ cuando el zombie pasa al estado de muerto ?. La respuesta es obvia, cuando pierde toda su vida. Pero ¿Desde qué estado puede pasar a muerto ?. La respuesta también es obvia. DESDE CUALQUIER ESTADO. Es decir, desde cualquier estado el Zombie puede morir. Por tanto necesitamos crear una transición desde AnyState a Zombie Death. Para ello pulsamos el botón derecho del ratón sobre el estado 'Any State' y escogemos la opción Make Transition.
Si pulsamos sobre el símbolo + de la sección Conditions aparecerá el único parámetro creado hasta ahora, indicando el valor que debe de tomar para pasar de una transición a otra. Como es de tipo trigger, cuando se active (pasa a true) cambiará de estado a Zombie Death. Este tipo de variable no se le indica que valor tiene que tomar para pasar de estado.
- La pregunta que te estás haciendo es, ¿ por qué hago uso de una variable de tipo Trigger y no de una de tipo Boolean ?
- Haz la prueba y comprueba lo que pasa.
- El problema estriba en que AnyState es cualquier estado aún sin acabar de ejecutarse la animación.
- Si haces uso de una variable booleana, el valor de dicha variable seguirá valiendo 'true' hasta que cambies nuevamente a 'false', por lo tanto, si hacemos la prueba gráficamente veremos que se ejecuta continuamente la animación de Zombie Death sin que acabe nunca.
- Esto se puede solucionar a nivel de programación, cambiando el valor de dicha variable a false inmediatamente después de ponerla a true (veremos como hacerlo). Pero esto es lo que hace la variable de tipo Trigger. Se pone a false inmediatamente después de true.
- Vamos a realizar ahora otro caso práctico que va a consistir en añadir un parámetro velocidad, para que en función del valor del parámetro cambie de estado de Zombie_Parado a Zombie_Walk y viceversa.
Marcando la transición, creamos una nueva condición añadiendo el parámetro 'velocidad' creado previamente e indicando que la transición tenga lugar cuando la velocidad tenga un valor mayor que 0.1f (por qué 0.1 y no 0. Pues porque al trabajar con números de tipo float, no tenemos un valor exacto debido a la precisión y redondeo de este tipo de números).
Modificando los parámetros por medio de scripts
- Antes de entrar en los scripts vamos a realizar unas pequeñas modificaciones al ejemplo.
- Vamos a dejar a los dos zombies sin rotar y cambiaremos la cámara de posición para que se vean más lejanos. Al hacerlo los zombies aparecerán de espaldas. Rotamos la cámara de sitio para que se vean por delante.
- Aumentaremos la escala del plano para que tengan más superficie por donde 'andar'.
- Como queremos que desde la animación de 'parado' pase a la animación de 'andar' sin que haya una transición, no chequeamos el parámetro Has Exit Time de la transición:
Si el parámetro Has Exit Time está chequeado, se habilita el parámetro Exit Time que es un valor entre 0-1 e indica en que porcentaje de la animación (en el ejemplo es la de Zombie_Parado) se va a poder pasar con una transición a la siguiente animación (Zombie_Walk). En el ejemplo, hasta que la animación de Zombie_Parado no llega al 90% de su reproducción, no pasaría a la animación de Zombie_Walk.
- Por medio de scripts vamos a poder acceder a los valores de los parámetros definidos en el AnimatorController.
- En la imagen hemos creado un script de nombre ControlEstadoZombie.
- Recordar colocarlo dentro de una carpeta de nombre Scripts y dentro de esta organizarlo como mejor creáis.
- Ahora editamos el código del script.
1 using UnityEngine;
2
3 public class ControlEstadoZombie : MonoBehaviour {
4
5 [SerializeField]
6 public float velocidadMaxima;
7 [SerializeField]
8 public int vida;
9
10 private float velocidadActual = 0f;
11 private Animator animator;
12
13 // Se ejecuta cuando hacemos un reset del script desde la ventana Inspector de Unity
14 void Reset()
15 {
16 velocidadMaxima = 2;
17 vida = 5;
18 velocidadActual = 0;
19 }
20
21
22 // Use this for initialization
23 void Start () {
24 animator = GetComponent<Animator>();
25 }
26
27 // Update is called once per frame
28 void Update () {
29
30 }
31 }
- Líneas 11 y 24: Estas líneas definen un objeto de la clase Animator (de nombre animator) y en la línea 24 obtenemos el objeto Animator que tiene asociado el GameObject en el Inspector y se lo asignamos a la variable.
- Resto de líneas las explicaremos más adelante.
- A partir de este momento accedemos a todos los métodos y propiedades de la clase Animator.
- Como a nosotros nos interesa acceder a los parámetros creados, podemos acceder con:
- SetFloat(String param,float valor): Este método está sobrecargado. Modificamos el valor del parámetro de tipo float.
- SetBool(String param, boolean valor): Este método está sobrecargado. Modificamos el valor del parámetro de tipo boolean.
- SetInteger(String param, integer valor): Este método está sobrecargado. Modificamos el valor del parámetro de tipo integer.
- SetTrigger(String param): Este método está sobrecargado. Modificamos el valor del parámetro de tipo trigger a true e inmediatamente después volverá a false.
- Disponemos de los correspondientes métodos GetFloat, GetBool, GetInteger para obtener los valores actuales de los parámetros.
- Lo que queremos hacer es que cuando se produzca un evento (una colisión, una pulsación del ratón, que el zombie detecte nuestra presencia,....) cambiemos el estado de parado a moviéndose.
- Vamos a hacerlo con la tecla W (podéis hacerlo de cualquier otra forma).
1 void Update() {
2 if (Input.GetKeyDown(KeyCode.W)){
3
4 animator.SetFloat("velocidad", 1);
5 }else if (Input.GetKeyDown(KeyCode.S))
6 {
7 animator.SetFloat("velocidad", 0);
8 }
9 }
- Liñas 4 y 7: Cambiamos el valor del parámetro 'velocidad' en función de si pulsamos la tecla W o la tecla S
- Si ejecutamos este código vemos que funciona, pero que el zombie no se mueve debido a que no estamos modificando su posición como ya vimos en esta Wiki.
- Modificamos el código para hacerlo:
1 void Update() {
2 transform.Translate(transform.forward * velocidadActual * Time.deltaTime);
3 if (Input.GetKeyDown(KeyCode.W)){
4 animator.SetFloat("velocidad", 1);
5 velocidadActual = 1;
6 }else if (Input.GetKeyDown(KeyCode.S))
7 {
8 animator.SetFloat("velocidad", 0);
9 velocidadActual = 0;
10 }
11 }
12
13 // Update is called once per frame
14 void Update() {
15
16 transform.Translate(transform.forward * velocidadActual * Time.deltaTime);
17
18 }
- Ahora vamos a complicarlo un poco y vamos a hacer que el zombie tenga diferentes velocidades, es decir, que se pueda mover más rápido.
- Podemos hacerlo cambiando el código:
1 void Update() {
2 transform.Translate(transform.forward * velocidadActual * Time.deltaTime);
3 if (Input.GetKeyDown(KeyCode.W) && velocidad<5){
4 animator.SetFloat("velocidad", 1);
5 velocidadActual++;
6 }else if (Input.GetKeyDown(KeyCode.S) && velocidad > 0)
7 {
8 animator.SetFloat("velocidad", 0);
9 velocidadActual--;
10 }
11 }
- Pero para hacerlo 'mas bonito' vamos a emplear un control gráfico HorizontalSlider.
- Para ello creamos un nuevo método OnGUI:
1 public class ControlEstadoZombie : MonoBehaviour {
2
3 [SerializeField]
4 public float velocidadMaxima;
5 [SerializeField]
6 public int vida;
7
8 private float velocidadActual = 0f;
9 private Animator animator;
10 private float m_MySliderValue;
11
12 void Reset()
13 {
14 velocidadMaxima = 2;
15 vida = 5;
16 velocidadActual = 0;
17 }
18
19 // Use this for initialization
20 void Start () {
21 animator = GetComponent<Animator>();
22 }
23
24 void OnGUI()
25 {
26 GUI.Label(new Rect(0, 25, 40, 60), "Speed");
27
28 // Devuelve un float en un rango entre 0.0f y velocidadMaxima
29 m_MySliderValue = GUI.HorizontalSlider(new Rect(45, 25, 200, 60), m_MySliderValue, 0.0f, velocidadMaxima);
30
31 velocidadActual = m_MySliderValue;
32 animator.SetFloat("velocidad", velocidadActual);
33 }
34
35 // Update is called once per frame
36 void Update() {
37
38 transform.Translate(transform.forward * velocidadActual * Time.deltaTime);
39
40 }
41
42 }
- Línea 4: Esta variable va a indicar la velocidad máxima a la que vamos a poder mover el zombie.
- Líneas 24-33: Dibuja el Slider y en función de su valor ajusta la velocidad del zombie
- Línea 16: Dibuja un label con texto speed.
- Línea 19: Dibuja un Horizontal Slider con un rango entre 0.0f y velocidadMaxima. Devuelve el valor seleccionado gráficamente.
- Línea 31: Asignamos la velocidad para la función translate del transform.
- Línea 32: Cambiamos la velocidad del parámetro. No importa su valor, solo si es mayor que 0.1f
- Ahora el problema lo tenemos en que la velocidad del zombie no se corresponde con la 'velocidad del movimiento'. Gráficamente se mueve muy lento cuando aceleramos.
- Para solucionarlo podemos hacer uso de la propiedad Speed
- Lo que queremos es que el valor de esa propiedad cambie con la velocidad.
- Como a nivel de programación disponemos del objeto 'animator' que representa toda la máquina de estados, podemos modificar su valor con el siguiente código:
1 void OnGUI()
2 {
3 GUI.Label(new Rect(0, 25, 40, 60), "Speed");
4
5 // Devuelve un float en un rango entre 0.0f y velocidadMaxima
6 m_MySliderValue = GUI.HorizontalSlider(new Rect(45, 25, 200, 60), m_MySliderValue, 0.0f, velocidadMaxima);
7
8 velocidadActual = m_MySliderValue;
9 animator.SetFloat("velocidad", velocidadActual);
10 animator.speed = m_MySliderValue;
11 }
- Si ejecutáis el juego podéis comprobar como al aumentar la velocidad aumenta la velocidad de la animación.
- El siguiente paso a programar podría ser cuando el zombie muere.
- Normalmente tendréis una determinada vida y al acabar con ella el zombie (o el protagonista) deben morir.
- Morir en el caso de los enemigos suele llevar el que desaparezca.
- Vamos a programar que el zombie tenga una vida. Al pulsar la tecla 'M' vamos a quitar una vida al zombie. Al llegar a cero vamos a hacer que muera.
1 void OnGUI()
2 {
3 if (vida > 0)
4 {
5 GUI.Label(new Rect(0, 25, 40, 60), "Speed");
6
7 // Devuelve un float en un rango entre 0.0f y velocidadMaxima
8 m_MySliderValue = GUI.HorizontalSlider(new Rect(45, 25, 200, 60), m_MySliderValue, 0.0f, velocidadMaxima);
9
10 velocidadActual = m_MySliderValue;
11 animator.SetFloat("velocidad", velocidadActual);
12 animator.speed = m_MySliderValue;
13 }
14 }
15
16 // Update is called once per frame
17 void Update() {
18 if (vida > 0)
19 {
20 transform.Translate(transform.forward * velocidadActual * Time.deltaTime);
21 }
22
23 /*
24 if (Input.GetKeyDown(KeyCode.W)){
25 animator.SetFloat("velocidad", 1);
26 velocidadActual = 1;
27 }else if (Input.GetKeyDown(KeyCode.S))
28 {
29 animator.SetFloat("velocidad", 0);
30 velocidadActual = 0;
31 }
32 */
33
34 if (Input.GetKeyDown(KeyCode.M))
35 {
36 vida--;
37 Debug.Log("Vida" + vida);
38 if (vida == 0) // Podemos usar == ya que es de tipo int
39 {
40 animator.speed = 1;
41 animator.SetTrigger("muerto");
42 }
43 }
44 }
45 }
- Si ejecutáis el código y presionáis la tecla M cinco veces, el zombie morirá.
- Línea 43: Fijarse que tenemos que volver a poner la velocidad a su valor por defecto, ya que sino, cuando inicie la animación del Zombie_Death lo haría a la velocidad establecida cuando el zombie andaba.
- Ahora solo queda hacer que el zombie desaparezca.
- Para ello hay que hacer uso del método GetCurrentAnimatorStateInfo(int layerIndex); el cual devuelve un objeto de la clase AnimatorStateInfo y que nos da información sobre la animación en curso.
- Una de las propiedades es normalizedTime el cual devuelve un valor entre 0-1 indicando cuando llega a 1 que la animación terminó.
- Uno de sus métodos es isName(String Nombre_animación) el cual devuelve un true/false si el animator está ejecutando la animación indicada en el parámetro.
- Para destruir el zombie tengo que preguntar si estoy en la animación 'Zombie_Death' y se ha acabado de ejecutar. Eso traducido a programación es:
1 // Update is called once per frame
2 void Update() {
3 if (vida > 0)
4 {
5 transform.Translate(transform.forward * velocidadActual * Time.deltaTime);
6 }
7
8 /*
9 if (Input.GetKeyDown(KeyCode.W)){
10 animator.SetFloat("velocidad", 1);
11 velocidadActual = 1;
12 }else if (Input.GetKeyDown(KeyCode.S))
13 {
14 animator.SetFloat("velocidad", 0);
15 velocidadActual = 0;
16 }
17 */
18
19 if (Input.GetKeyDown(KeyCode.M))
20 {
21 vida--;
22 Debug.Log("Vida" + vida);
23 if (vida == 0) // Podemos usar == ya que es de tipo int
24 {
25 animator.speed = 1;
26 animator.SetTrigger("muerto");
27 }
28 }
29
30 if (animator.GetCurrentAnimatorStateInfo(0).IsName("Zombie_Death") && animator.GetCurrentAnimatorStateInfo(0).normalizedTime>=1f)
31 {
32 Destroy(gameObject);
33 }
34 }
- Métodos útiles:
- animator.GetCurrentAnimatorStateInfo(0).IsName("Zombie_Walk"): Devuelve true si el zombie se encuentra en la animación Zombie_Walk.
- El (0) hace referencia a las capas, las cuales se usan para animar de forma diferente a diferentes partes del cuerpo.
Opciones no vistas
Gestionando las animaciones. Animatior Override Controller
Creando animaciones propias
Modificando el orden de los componentes
- Si una animación modifica la posición-rotación, estas modificaciones van a sobre-escribir las modificaciones que tengamos en nuestros scripts.
- Para evitarlo podemos:
- Crear un GameObject vacío y hacer que el modelo sea 'hijo' de este GameObject, asociando nuestro script al GameObject vacío.
- Cambiar el orden de los componentes en la Inspector Window y hacer que el script se ejecute antes, por lo que primero realizará nuestra rotación y después la rotación de la animación.
- Veamos un ejemplo:
-- Ángel D. Fernández González -- (2018).