Movimiento relativo

En este post haremos que el movimiento de nuestro personaje se asemeje más a un juego de aventuras de modo que se desplace con respecto a la posición de la cámara.

Antes de nada por el momento desactivaremos el script de nuestra cámara y la colocaremos en un sitio de modo que enfoque todo nuestro nivel.

Ahora empezaremos por conseguir la posición de la cámara mediante su componente Transform. Para ello crearemos una variable del tipo Transform a la que llamaremos cameraTransform (Transform cameraTransform;). Ahora en Start almacenaremos en esta variable la posición de nuestra cámara principal. Accediendo a la variable main de la clase Camera obtendremos directamente la primera cámara etiquetada como MainCamera. Como queremos únicamente su posición añadiremos .transform . El código sería el siguiente:

cameraTransform = Camera.main.transform;

A continuación eliminaremos o comentaremos parte de nuestro código, ya que deberemos rehacerlo. Para comentar varias líneas consecutivas podemos poner ‘/*` donde queremos empezar y poner ‘*/’ donde queremos acabar.

comentar

Ya en Update, crearemos una variable tipo Vector3 a la que llamaremos horizontalInput y la igualaremos a un new Vector3 que se compondrá de la entrada de Horizontal para el eje X, 0 para el eje Y y la entrada de Vertical para el eje Z : Vector3 horizontalInput = new Vector3 (Input.GetAxis ("Horizontal"), 0, Input.GetAxis ("Vertical"));

El valor máximo de HorizontalVertical será 1. Sin embargo si ambos son iguales a 1 la longitud del vector resultante será mayor que 1, por lo que nuestro personaje se desplazaría a mayor velocidad. Para evitar esto comprobaremos mediante la variable magnitude de la clase Vector3 si la longitud del vector es mayor que 1 y, de ser así, usaremos la variable normalized de la clase Vector3 que almacena el vector normalizado (con longitud igual a 1). El código resultante será el siguiente:

Vector3 horizontalInput = new Vector3 (Input.GetAxis ("Horizontal"), 0, Input.GetAxis ("Vertical"));
if (horizontalInput.magnitude > 1)
horizontalInput.Normalize ();

Necesitaremos trabajar sobre la variable horizontalInput, pero también la necesitamos sin modificar posteriormente. Por este motivo crearemos otro Vector3 al que llamaremos targetHorizontalInput al que igualaremos a horizontalInputVector3 targetHorizontalMovement = horizontalInput;

Lo siguiente que haremos será emplear la función TransformDirection de la clase Transform para adaptar las coordenadas locales de targetHorizontalMovement a las coordanadas globales de nuestra escena. Básicamente lo que hará será rotar el vector con respecto la posición de nuestra cámara:

targetHorizontalMovement = cameraTransform.TransformDirection (targetHorizontalMovement);

Otra manera de hacer lo mismo sería multiplicando por el Quaternion que representa la rotación de la cámara:

targetHorizontalMovement = cameraTransform.rotation * targetHorizontalMovement;

A continuación asignaremos a nuestro vector de movimiento currentMovement el valor de targetHorizontalMovement multiplicado por la velocidad a la que queremos que se desplace nuestro personaje (moveSpeed) :

currentMovement = targetHorizontalMovement * moveSpeed;

Si probamos cómo se comporta nuestro personaje ahora mismo veremos que se desplazará a izquierda y derecha respecto a la posición de la cámara. Del mismo modo irá adelante y atrás con respecto a la cámara, por lo que si la cámara está inclinada nuestro personaje también se moverá en el eje Y (arriba y abajo) al realizar este tipo de movimiento. Para evitar esto añadiremos previamente una asignación a targetHorizontalMovement para que su componente Y sea siempre 0 :

targetHorizontalMovement.y = 0;

Seguiremos teniendo un problema. La velocidad del personaje para el movimiento adelante-atrás se verá reducida ya que el vector que lo constituye de longitud igual a 1 perderá parte de su valor al eliminar uno de los componentes que lo forman, eje Y, al que igualamos a 0. El vector adquiere esta distribución de su valor en el eje Y después de hacer la conversión mediante la función TransformDirection. Esta pérdida será mayor cuanta más inclinación tenga la cámara. Deberemos normalizar de nuevo el vector para que vuelva a tener una velocidad normal y estable en todas las direcciones en las que se moverá con respecto a la posición de la cámara:

targetHorizontalMovement.Normalize ();

Si bien hemos solucionado nuestro problema, también hemos creado otro. Ahora no importa cual sea el valor de nuestro movimiento para nuestros ejes de entrada (HorizontalVertical), debido a esta normalización siempre será 1 ó 0. Si estuviésemos usando un joystick no habría diferencia entre moverlo un poco o moverlo hasta el punto máximo que nos permitiese. Para solucionar esto recurriremos al valor original que nos proporcionará horizontalInput y lo multiplicaremos por targetHorizontalMovement. De este modo, si por ejemplo la entrada original tiene un valor de 0,1, al ser multiplicado por 1 el nuevo valor de horizontalInput será 0,1 :

targetHorizontalMovement *= horizontalInput.magnitude;

El fallo a solucionar ahora es que nuestro personaje es incapaz de saltar aun si le damos al botón de salto (barra espaciadora). Esto se debe a que en cada frame actualizamos el valor del eje Y a 0. Para solucionar esto crearemos fuera de Update una variable de tipo float a la que llamaremos verticalSpeed (float verticalSpeed;) que nos servirá para almacenar el valor del eje Y de nuestro personaje en lo que respecta a la gravedad y el salto. Por lo tanto, en la parte de nuestro código donde trabajamos con esto sustituiremos currentMovement.y por verticalSpeed. Justo después añadiremos una línea donde asignaremos el valor de verticalSpeedcurrentMovement.y .

if (!controller.isGrounded)
verticalSpeed -= gravity * Time.deltaTime;
else
verticalSpeed = 0;

if (controller.isGrounded && Input.GetButtonDown ("Jump"))
verticalSpeed = jumpSpeed;

currentMovement.y = verticalSpeed;

code1MovRel

Si deseamos añadir al movimiento de nuestro personaje la fluidez por nuestra cuenta, al margen de la propia de Input, empezaremos por sustituir GetAxis por GetAxisRaw y seguiremos los pasos ya vistos en post previos usando, por ejemplo, la función SmoothDamp.

Nuestro código final quedará del siguiente modo:

using UnityEngine;
using System.Collections;

public class PlayerScript : MonoBehaviour {

public float moveSpeed = 5;
public float rotateSpeed = 180;
public float jumpSpeed = 20;
public float gravity = 9.8f;
public float moveSpeedSmooth = 0.3f;
public float rotateSpeedSmooth = 0.3f;

float currentForwardSpeed;
float forwardSpeedV;

float targetRotation;
float currentRotation;
float rotationV;

CharacterController controller;
Vector3 currentMovement;
Transform cameraTransform;
float verticalSpeed;

void Start () {

controller = GetComponent<CharacterController> ();
cameraTransform = Camera.main.transform;
}


void Update () {
/*
targetRotation += Input.GetAxisRaw ("Horizontal") * rotateSpeed * Time.deltaTime;
if (targetRotation > 360)
targetRotation -= 360;
if (targetRotation < 0)
targetRotation += 360;
currentRotation = Mathf.SmoothDampAngle (currentRotation, targetRotation, ref rotationV, rotateSpeedSmooth);
transform.eulerAngles = new Vector3 (0, currentRotation, 0);


currentForwardSpeed = Mathf.SmoothDamp (currentForwardSpeed, Input.GetAxisRaw ("Vertical") * moveSpeed, ref forwardSpeedV, moveSpeedSmooth);

currentMovement = new Vector3 (0, currentMovement.y, currentForwardSpeed);
currentMovement = transform.rotation * currentMovement;
*/


Vector3 horizontalInput = new Vector3 (Input.GetAxis ("Horizontal"), 0, Input.GetAxis ("Vertical"));
if (horizontalInput.magnitude > 1)
horizontalInput.Normalize ();
Vector3 targetHorizontalMovement = horizontalInput;
targetHorizontalMovement = cameraTransform.TransformDirection (targetHorizontalMovement);
//targetHorizontalMovement = cameraTransform.rotation * targetHorizontalMovement;
targetHorizontalMovement.y = 0;
targetHorizontalMovement.Normalize ();
targetHorizontalMovement *= horizontalInput.magnitude;
currentMovement = targetHorizontalMovement * moveSpeed;

if (!controller.isGrounded)
verticalSpeed -= gravity * Time.deltaTime;
else
verticalSpeed = 0;

if (controller.isGrounded && Input.GetButtonDown ("Jump"))
verticalSpeed = jumpSpeed;

currentMovement.y = verticalSpeed;

controller.Move (currentMovement * Time.deltaTime);
}
}

Raycast

En este post aprenderemos a hacer que nuestra cámara no ignore los objetos a la hora de moverse con el personaje.

Si detrás de nuestro personaje hay un bloque en el aire debemos hacer que nuestra cámara lo tenga en cuenta en lugar de simplemente atravesarlo y mostrarnos una imagen como si dicho bloque no existiera. Para ello recurriremos a la función Raycast de la clase Physics. La función Raycast devolverá true si el rayo que la compone atraviesa algún collider (componente que indica colisiones: box collidercharacter controller…), de otro modo devolverá false. Ese rayo irá de un punto a otro en línea recta y será invisible. En nuestro caso haremos que nuestra cámara lance el Raycast hacia nuestro personaje y nos indicará si hay algo entre él y la cámara.

Por defecto el rayo detectará todo lo que tenga alrededor. La cápsula que compone a nuestro personaje tiene un collider, por lo que el rayo detectará una colisión. Para evitar esto tenemos varias opciones. La más sencilla es seleccionar nuestro personaje y cambiar el layer del que forma parte a Ignore Raycast. Raycast ignorará todo lo que haya en la capa que le indiquemos (por defecto Ignore Raycast).

IgnoreRaycast

Hay varios modos de uso para la función Raycast. Nosotros nos centraremos en dos. En el primero necesitaremos cuatro variables para la función. La primera será origin, que indicará el origen del rayo (nuestro personaje). La siguiente será direction, que nos dirá la dirección que seguirá el rayo (será igual a la resta de la posición de la cámara y la posición de nuestro personaje). Ambas serán variables del tipo Vector3. La tercera será distance, una variable de tipo float que marcará la distancia que recorrerá el rayo (distancia entre el personaje y la cámara). Para medir esa distancia usaremos la función Distance de la clase Vector3. En estas dos variables usaremos la posición futura de nuestra cámara (targetMove) en lugar de la posición actual (transform.position). La última, layerMask, indicará si queremos ignorar alguna capa, en nuestro caso la capa Ignore Raycast a la que asignamos a nuestro personaje. El código para este caso sería así:
Physics.Raycast(player.position, targetMove - player.position, Vector3.Distance(player.position,targetMove))

El segundo modo de uso que aprenderemos introducirá despues de direction la variable hitInfo, del tipo RaycastHit. Si la función devuelve true, la variable hitInfo contendrá la información acerca de dónde el collider fue golpeado (ver RaycastHit en ScriptReference para saber qué información almacena). En nuestro caso nos interesará la información de la posición. Necesitaremos crear una variable del tipo RaycastHit dentro de Update (sólo será accesible dentro del propio Update) a la que llamaremos hit. Dentro de la función pondremos nuestra variable precedida de out (out hit), de modo que devolverá esa información a nuestra variable hit. El código para este caso sería así:

Physics.Raycast (player.position, targetMove - player.position, out hit, Vector3.Distance (player.position, targetMove))

Crearemos una sentecia if que en caso de que Raycast devuelva true almacene la posición del objeto con el que el rayo colisiona en la variable targetMove, de modo que esa será la nueva posición a la que se desplazaría la cámara. Esta sentencia debe estar después de la asignación de la posición futura de la cámara en targetMove. El código sería el siguiente:

RaycastHit hit;
if (Physics.Raycast (player.position, targetMove - player.position, out hit, Vector3.Distance (player.position, targetMove))) {
targetMove = hit.point;
}

Ahora la cámara ajustará su posición si hay objetos bloqueando la visión a nuestro personaje. Sin embargo, ya que el punto que toma estará pegado al objeto, la cámara dará la sensación de estar atravesando el objeto. Para solucionar esto podemos modificar el valor del parámetro clipping planes en nuestra cámara. A continuación se mostrarán dos imágenes para apreciar la diferencia de tener un valor alto o uno bajo:

ClippingPlanesHigh ClippingPlanesLow

Además adelantaremos un poco la posición de la cámara en el caso de situarse en el punto de colisión con un objeto para que no se sitúe pegada a dicho objeto. Para ello usaremos la función Lerp de la clase Vector3 en targetMove = hit.point; . Como posición de origen usaremos el propio hit.point, como posición de destino nuestro personaje. Para la variable t crearemos una variable pública de tipo float a la que llamaremos rayHitMoveInFront y le asignaremos un valor de 0,1 (public float rayHitMoveInFront = 0.1f;). El código quedará así:

targetMove = Vector3.Lerp(hit.point, player.position, rayHitMoveInFront);

Con esto lo que hará la cámara será situarse en un punto intermedio entre el punto de colisión con el objeto y el personaje. Esta posición intermedia vendrá determinada por el valor que le asignemos a rayHitMoveInFront, cuanto mayor sea más se acercará a nuestro personaje.

Nuestro código final tras finalizar este post quedará del siguiente modo:

using UnityEngine;
using System.Collections;

public class CameraScript : MonoBehaviour {

Transform player;
Quaternion targetLook;
Vector3 targetMove;
public float smoothLook = 0.5f;
public float smoothMove = 0.5f;
Vector3 smoothMoveV;
public float distFromPlayer = 5;
public float heightFromPlayer = 3;
public float rayHitMoveInFront = 0.1f;

void Start () {

player = GameObject.FindWithTag ("Player").transform;
}
void Update () {

targetMove = player.position + (player.rotation * new Vector3 (0, heightFromPlayer, -distFromPlayer));

RaycastHit hit;
if (Physics.Raycast (player.position, targetMove - player.position, out hit, Vector3.Distance (player.position, targetMove))) {
targetMove = Vector3.Lerp(hit.point, player.position, rayHitMoveInFront);
}

//transform.position = player.position + (player.rotation * new Vector3 (0, heightFromPlayer, -distFromPlayer));

//transform.position = Vector3.Lerp (transform.position, targetMove, smoothMove * Time.deltaTime);
transform.position = Vector3.SmoothDamp (transform.position, targetMove, ref smoothMoveV, smoothMove);

targetLook = Quaternion.LookRotation (player.position - transform.position);
transform.rotation = Quaternion.Lerp (transform.rotation, targetLook, smoothLook * Time.deltaTime);

//transform.LookAt (player);
}
}

RayCastCode

Fluidez de cámara 2

En este post aprenderemos a hacer que nuestra cámara se mantenga detrás de nuestro personaje y lo siga de manera que veamos lo que tiene delante en todo momento.

Empezaremos creando dos variables. La primera será una variable pública de tipo float a la que llamaremos distFromPlayer y le asignaremos un valor igual a 5 (public float distFromPlayer = 5;). Esta variable indicará la distancia nuestro personaje de la cámara. La segunda variable será también una variable pública de tipo float a la que llamaremos heightFromPlayer y le asignaremos un valor igual a 3 (public float heightFromPlayer = 3;). Esta variable indicará la altura a la que estará la cámara.

A continuación haremos que la posición de la cámara sea siempre la misma que la del personaje pero desplazada siguiendo el valor que le asignemos a las variables anteriormente creadas. Para ello igualaremos la posición de la cámara con la del personaje más un vector con los valores para la altura en el eje Y (heightFromPlayer) y la distancia en el eje Z (distFromPlayer) a la que queremos que esté. El código para esto será: transform.position = player.position + new Vector3 (0, heightFromPlayer, -distFromPlayer); .

El problema es que esto solamente mantendrá la cámara siguiendo al personaje a una distancia fija pero no rotará con él, de manera que no se mantendrá detrás del personaje. Para que rote con el personaje tendremos que multiplicar la rotación del mismo por el vector. De modo que quedará transform.position = player.position + (player.rotation * new Vector3 (0, heightFromPlayer, -distFromPlayer)); . Ahora la cámara se mantendrá siempre detrás del personaje aunque este rote. Para variar la velocidad a la que la cámara se adapta solo tendremos que aumentar el valor de la variable smoothLook. Un valor igual a 7 sería una buena opción.

followPlayer

Ahora mejoraremos el comportamiento y fluidez de la cámara en su desplazamiento de posición del mismo modo que hicimos con la rotación. Crearemos una variable Vector3 a la que llamaremos targetMove (Vector3 targetMove;) donde almacenaremos la posición objetivo de nuestra cámara. Para ello simplemente modificaremos la línea de código que teníamos previamente de modo que cambiaremos transform.position por nuestro vector targetMove. Quedará del siguiente modo: targetMove = player.position + (player.rotation * new Vector3 (0, heightFromPlayer, -distFromPlayer)); . Del mismo modo que hicimos con la rotación usaremos la función Lerp, pero esta vez mediante la clase Vector3. Para la variable from usaremos transform.position (nuestra posición de origen), para la variable to usaremos targetMove (nuestra posición objetivo) y para t emplearemos smoothMove, una variable pública que crearemos del tipo float con un valor inicial que le asignaremos igual a 0,5 (public float smoothMove = 0.5f;), a la que escalaremos en el tiempo empleando Time.deltaTime . La línea de código quedará de la siguiente manera: transform.position = Vector3.Lerp (transform.position, targetMove, smoothMove * Time.deltaTime); . Podéis probar a sustituir la función Lerp por Slerp, el resultado será idéntico.

followPlayer2

SmoothCam

Para lograr un efecto similar, pero probablemente con incluso más fluidez, podemos emplear la función SmoothDamp de la clase Vector3 que generará un Vector3. Comentaremos la anterior línea de código si no queremos eliminarla antes de empezar con la nueva. Como ya hemos visto necesitaremos un Vector3 para indicar la posición actual, otro para la posición objetivo, otro vector de referencia para la velocidad y una variable tipo float para el tiempo. Para la posición actual usaremos transform.position, mientras que para la posición objetivo targetMove. Crearemos una variable de tipo Vector a la que llamaremos smoothMoveV para usar como variable de referencia para la velocidad. Para la variable del tiempo que queremos que tarde en realizar la transición usaremos smoothMove. Como esta variable indicará el tiempo de por sí, no necesitamos escalarlo con Time.deltaTime. Además, en este caso cuanto mayor sea el número mayor será el tiempo que tarde la cámara en moverse, a diferencia de Lerp donde cuanto mayor fuese t menor sería el tiempo en realizar el movimiento. La línea de código resultante sería la siguiente: transform.position = Vector3.SmoothDamp (transform.position, targetMove, ref smoothMoveV, smoothMove); . Debemos reajustar el valor de la variable smoothMove a un valor menor (por ejemplo 0,2).

followPlayer3

El código final que obtenemos realizando los pasos del post será:

using UnityEngine;
using System.Collections;

public class CameraScript : MonoBehaviour {

Transform player;
Quaternion targetLook;
Vector3 targetMove;
public float smoothLook = 0.5f;
public float smoothMove = 0.5f;
Vector3 smoothMoveV;
public float distFromPlayer = 5;
public float heightFromPlayer = 3;

void Start () {

player = GameObject.FindWithTag ("Player").transform;
}

void Update () {

//transform.position = player.position + (player.rotation * new Vector3 (0, heightFromPlayer, -distFromPlayer));

targetMove = player.position + (player.rotation * new Vector3 (0, heightFromPlayer, -distFromPlayer));
//transform.position = Vector3.Lerp (transform.position, targetMove, smoothMove * Time.deltaTime);
transform.position = Vector3.SmoothDamp (transform.position, targetMove, ref smoothMoveV, smoothMove);

targetLook = Quaternion.LookRotation (player.position - transform.position);
transform.rotation = Quaternion.Lerp (transform.rotation, targetLook, smoothLook * Time.deltaTime);

//transform.LookAt (player);
}
}

Fluidez de cámara

En este post explicaremos cómo lograr que la cámara se comporte de un modo más «vivo», con movimientos no tan rígidos como si se encontrase pegada al personaje. Para este caso usaremos la función Lerp de la clase Mathf. A este función habrá que pasarle tres variables de tipo float y la función interpolará entre la primera (from) y la segunda (to) mediante la tercera (t). t estará comprendida entre 0 y 1. Para los valores mayores a 1 tomará 1 como valor. Si t es 0 el valor que tomará la función será from y si t es 1 tomará el valor de to. Si t fuese 0,5 el valor tomado sería el valor medio de from y to. La siguiente gráfica explica es comportamiento lineal de la función con respecto a las tres variables:

Lerp

En nuestro código la variable from, que será nuestro posición actual, variará en cada frame, mientras que tto se mantendrá constante. De este modo, si por ejemplo usamos t = 0,5, to = 20 y from = 10, se tomará el valor 15. Si repetimos el proceso tomando la nueva posición el valor de from pasará a ser 15. El valor medio entre tofrom será 17,5 ahora y to pasará a valer 17,5. Y así sucesivamente. Esto es lo que hará que nuestra cámara se comporte de un modo más natural. Si bien es cierto que matemáticamente nunca se llegaría al valor final (20 en este caso), el ordenador, llegado a un punto donde el número de decimales es muy abultado, acabaría redondeando a dicho valor final.

La función SmoothStep (o Slerp) de la misma clase Mathf funcionará de un modo similar a Lerp pero suavizando la interpolación en los límites tanto inferior como superior de la función. La siguiente gráfica explicará el comportamiento aproximado de esta función:

Slerp

En nuestro ejemplo pasado esta función funcionará de manera idéntica a Lerp, sin ser casi apreciable una diferencia entre una u otra. Además ambas funciones podrán trabajar con vectores en lugar de variables float, de modo que cada componente de estos vectores que marcan las posiciones fromto se interpolarán entre sus correspondientes valores del mismo eje. Debido a este podremos interpolar entre dos puntos en el espacio. También sucederá lo mismo cuando se trate de la rotación, aunque en este caso tendremos que trabajar con la clase Quaternion. Los quaternions se usan para representar la rotación de un modo mas complejo y preciso que con el uso de grados euler. Dentro de la clase Quaternion disponemos también de las funciones Lerp y Slerp. Funcionarán del mismo modo, simplemente que usarán quaternions.

Ahora empezaremos a crear el código. Lo primero que haremos será eliminar (o convertir en comentario mediante ‘//’) la línea transform.LookAt (player); . A continuación crearemos una variable de tipo Quaternion a la que llamaremos targetLook (Quaternion targetLook;). En esta variable almacenaremos la rotación que necesita nuestra cámara para apuntar al objetivo. Para ello la igualaremos al resultado dado por la función LookRotation de la clase Quaternion. A esta función se le pasarán dos vectores. El primero indicará la dirección hacia la que queremos mirar y el segundo indicará en que dirección está arriba (si no ponemos nada por defecto será Vector3.up). La función devolverá un Quaternion con la información de la rotación necesaria para enfocar hacia el objetivo y lo almacenaremos en la variable targetLook. Para este vector de dirección debemos tener en cuenta la posición de la cámara, ya que no se encuentra en el punto (0,0,0). Para ese restaremos a la posición de nuestro personaje la posición de la cámara. Para acceder a la posición de la cámara, puesto que el script será un componente de la misma, solo tendremos que escribir transform.position . La línea de código para esta acción quedaría así: targetLook = Quaternion.LookRotation (player.position - transform.position); .

Ya sólo tendríamos que asignar a la rotación de la cámara los valores de targetRotation escribiendo transform.rotation = targetLook; . Sin embargo, como queremos un movimiento más natural y fluido de nuestra cámara, usaremos la función Lerp para la transición entre el valor de rotación actual y el valor de rotación objetivo de una manera suave. La línea de código sería transform.rotation = Quaternion.Lerp (transform.rotation, targetLook, smoothLook * Time.deltaTime); . transform.rotation indica la rotación actual y targetLook incica la rotación objetivo. Para graduar la interpolación entre ambos valores hemos creado la variable pública de tipo float a la que llamaremos smoothLook con un valor inicial de 0.5 (public float smoothLook = 0.5f;).  Como no sabemos el framerate del ordenador donde se ejecutará el juego tenemos que escalarlo con respecto al tiempo multiplicándolo por la variable Time.deltaTime.

Lerpcode

Ya tenemos la cámara diseñada para un movimiento más fluido y natural. Podemos variar la variable smoothLook para aumentar o disminuir la velocidad a la que la cámara rota para mirar hacia el personaje. Podéis probar la diferencia entre Lerp y Slerp (simplemente cambiad Lerp por Slerp), aunque probablemente obtengáis un resultado idéntico. El código final de este post será el siguiente:

using UnityEngine;
using System.Collections;

public class CameraScript : MonoBehaviour {

Transform player;
Quaternion targetLook;
public float smoothLook = 0.5f;

void Start () {

player = GameObject.FindWithTag ("Player").transform;
}

void Update () {

targetLook = Quaternion.LookRotation (player.position - transform.position);
transform.rotation = Quaternion.Lerp (transform.rotation, targetLook, smoothLook * Time.deltaTime);

//transform.LookAt (player);
}

}

Movimiento de cámara

En este post aprenderemos cosas relacionadas con la cámara. Empezaremos creando un script (en Project dentro de nuestra carpeta Scripts, Create->C# Script) para nuestra cámara al que llamaremos CameraScript. Sacaremos la Main Camera de dentro de nuestro Player y le añadiremos nuestro script CameraScript.

camerascript

A continuación abriremos el script y crearemos una variable pública del tipo de la clase Transform y la llamaremos player (public Transform player;). En esta variable almacenaremos la información del complemento Transform de nuestro personaje Player. Para hacer esto tenemos dos opciones. La primera es arrastrar desde Hierarchy el objeto Player hacia el recuadro de la variable player que aparece en Inspector en nuestra cámara. Otra forma es hacer clic en el botón que está a la derecha del recuadro que abrirá una ventana con todos los objetos de la scene. En esta ventana seleccionamos Player y aparecerá en el recuadro. Si hacemos clic en el recuadro marcará en amarillo a que objeto corresponde su contenido.

playertransform

Ahora usaremos la función LookAt de la clase Transform que rotará el objeto apuntando hacia el objetivo que le marquemos. Recordad que el uso de funciones y variables de las clases afectarán al objeto al que esté asociado el script. A la función LookAt se le pasarán dos datos: una variable del tipo clase Transform o un Vector3 para indicar la posición objetivo y un Vector3 para indicar la posición desde la que se mirará al objetivo. Si no ponemos ningún vector para la segunda variable tomará por defecto el vector Vector3.up. La línea de código resultante será: transform.LookAt (player); .

lookatcode

Lo siguiente que haremos será marcar nuestro personaje con la etiqueta Player en Tag. Ahora podremos hacer que nuestro script obtenga los datos de Transform de Player sin necesidad de que lo arrastremos  a nuestro script en Inspector en cada nivel que tengamos. Para ello recurriremos a la función FindWithTag de la clase GameObject que devolverá un objeto etiquetado con el nombre de la etiqueta que le pasemos a dicha función, en nuestro caso Player. Como no necesitaremos acceder desde Unity a la variable player la convertiremos en privada. En la parte de Start de nuestro script almacenaremos en nuestra variable player de la clase Transform los datos del componente Transform de nuestro personaje Player, que estará etiquetado además con el tag Player. Para almacenarlo recurriremos a la mencionada función FindWithTag para que nos devuelva el objeto Player como resultado de modo que tendríamos que escribir: player = GameObject.FindWithTag ("Player"); . Sin embargo esto nos devolvería el objeto entero y nosotros solo queremos su componente Transform, por lo que debemos añadir .transform de modo que quedaría: player = GameObject.FindWithTag ("Player").transform; . Recordad que cuando se trata de una clase o una función debemos empezar por letra mayúscula (GameObject), mientras que si se trata de una variable será con minúscula (gameObject).

findwithtag

El código resultante hasta ahora será el siguiente:

using UnityEngine;
using System.Collections;

public class CameraScript : MonoBehaviour {

Transform player;

void Start () {

player = GameObject.FindWithTag ("Player").transform;
}

void Update () {

transform.LookAt (player);
}
}

 

Con lo que hemos hecho nuestra cámara debería quedarse en su posición inicial sin desplazarse, pero rotando de manera que enfoque siempre al objetivo (el personaje en nuestro caso).

Si por error borráis alguna vez algún script, material o demás en la pestaña Project, recordad que podréis recuperarlo en la papelera de reciclaje de vuestro escritorio.

Smoothdamp

En este post hablaremos un poco más acerca de Input y de cómo usar InputManager. Como ya hemos visto para abrir el InputManager iremos a Edit->Project Settings->Input y aparecerá la pestaña InputManager donde teníamos Inspector. Aquí aparecerán las entradas asignadas a nuestro juego y las teclas con las que se corresponden. Si aumentamos el valor de Size aparecerán nuevas entradas cuyos nombres serán iguales a la última entrada que teníamos (en mi caso Cancel). Podemos editar cada entrada, ya sea nombre, teclas asignadas, etc. Editemos una entrada para crear un botón de pausa. Le cambiaremos el nombre por Pause y le asignaremos la tecla escape. Para ello debemos escribir correctamente la tecla que queremos asignar y darle a enter. Si hemos escrito mal el nombre de la tecla el recuadro se pondrá en blanco.

inputmanager

Si ahora reducimos el número de Size se eliminará las últimas entradas de la lista hasta llegar al número indicado.

Si observamos nuestro código veremos que el nombre de las entradas tenemos asignadas a las funciones de la clase Input, por ejemplo «Horizontal», se corresponden con alguna de nuestras entradas de InputManager. Para hacer uso de una entrada debemos escribir su nombre exactamente igual a como aparece en InputManager. Tendremos un Positive Button y un Negative Button. El primero nos dará un valor positivo, mientras que el segundo nos dará un valor negativo con respecto al primero. Si no pulsamos ninguno el resultado será 0. Esto sería un eje (Axis). Si suprimimos el valor del Negative Button esta entrada pasaría a ser simplemente un botón que devolverá true si lo pulsamos o false si no. Tendremos también la posibilidad de asignar un botón alternativo para cada uno que incluirán al principio de su nombre «Alt». Estos botones realizarán la misma función que los anteriores pero en teclas distintas. La siguiente opción, Gravity, indicará cuántas unidades por segundo requerirá para que la entrada vuelva a 0, mientras que la opción Sensitivity será lo mismo pero para que la entrada alcance su valor máximo. En el caso de Horizontal cuanto más alto sea el valor de Gravity menor será el tiempo que tardará el objeto en detener su giro, mientras que cuanto mayor sea el valor de Sensitivity menor será el tiempo en girar a la velocidad máxima. Si tenemos activada la opción Snap, al cambiar de uno de los botones (positivo o negativo) al otro el valor del eje pasará a ser 0 directamente para alcanzar el valor máximo que indique el botón que estemos presionando. Sin la opción Snap activada esta variación se hará desde el valor máximo de uno de los botones hasta el valor máximo del otro (el lugar que desde 0). La opción Dead será la que indique el tamaño de la zona muerta de un joystick, esto es la distancia que debe recorrer el joystick antes de que se tenga en cuenta el valor de su entrada. La opción Invert intercambiará los valores del botón positivo y botón negativo.

Horizontal

En Type tendremos tres opciones. «Key or Mouse Button» para la teclas del teclado o los botones del ratón. «Mouse Movement» para el movimiento realizado con el ratón. «Joystick Axis» para el uso de los joysticks de, por ejemplo, un gamepad de Xbox. En Axis podremos elegir el eje sobre el que trabajará nuestra entrada. Por último, en Joy Num tendremos que elegir para que joystick estarán asignada esta entrada. Con la opción «Get Motion from all Joysticks» se asignará a todos los posibles.

Si en vez de usar la función GetAxis empleamos la función GetAxisRaw, la entrada no tendrá en cuenta las opciones para hacer más fluido el movimiento (Gravity, Dead, Sensitivity, Snap). Nosotros mismos nos encargaremos de hacer que el movimiento de nuestro personaje sea más real y se sienta menos rígido. Emplearemos la función Smoothdamp de la clase Mathf para realizar el cambio gradual del movimiento de nuestro personaje. Empezaremos creando una variable privada de tipo float a la que llamaremos currentForwardSpeed con un valor por defecto igual a 0  (float currentForwardSpeed;) para usar como nuestro movimiento adelante-atrás. Para la posición que queremos alcanzar usaremos Input.GetAxisRaw ("Vertical") * moveSpeed , que tomará el valor de la entrada «Vertical». Crearemos una variable tipo float a la que llamaremos forwardSpeedV que será nuestra variable de referencia para la velocidad actual en la función Smoothdamp. Crearemos una variable pública tipo float a la que llamaremos moveSpeedSmooth a la que igualaremos a 0,3 (public float moveSpeedSmooth = 0.3f;) para indicar el intervalo del tiempo que queremos que tarde en realizar la transición desde el valor de currentForwardSpeed al valor de la entrada «Vertical». Puesto que la variable de nuestro movimiento adelante-atrás será currentForwardSpeed, la línea de código en la que usaremos la función Smoothdamp quedará así : currentForwardSpeed = Mathf.SmoothDamp (currentForwardSpeed, Input.GetAxisRaw ("Vertical") * moveSpeed, ref forwardSpeedV, moveSpeedSmooth); . Y en la línea de código donde asignamos los valores del vector currentMovement sustituiremos Input.GetAxisRaw ("Vertical") * moveSpeed por la variable currentForwardSpeed, de modo que tendremos la siguiente línea: currentMovement = new Vector3 (0, currentMovement.y, currentForwardSpeed); .

currentForwardSpeed

Para la rotación emplearemos SmoothDampAngle, que será muy similar solo que adaptado a ángulos.  Empezaremos por conseguir cual es la rotación que deseamos. Crearemos una variable privada tipo float a la que llamaremos targetRotation que servirá para almacenarla (float targetRotation;).  A esta variable le sumaremos la entrada «Horizontal» que es la encargada de la rotación en nuestro juego, para ello escribiremos la siguiete línea de código: targetRotation += Input.GetAxisRaw ("Horizontal") * rotateSpeed * Time.deltaTime; . Debemos tener en cuenta que el valor del ángulo se encontrará siempre entre 0 y 360, por ello mediante dos sentencias if comprobaremos si es menor que 0 o mayor que 360. Si es mayor que 360 le restaremos 360, mientras que si es menor que 0 le sumaremos 360:

if (targetRotation > 360)
targetRotation -= 360;
if (targetRotation < 0)
targetRotation += 360;

A continuación crearemos una variable privada tipo float a la que llamaremos currentRotation que nos indicará la posición actual de rotación que también usaremos para rotar al personaje (float currentRotation;). También necesitaremos una variable privada tipo float a la que llamaremos rotationV (float rotationV;)  que indique la velocidad actual de referencia para SmoothDampAngle y una variable pública tipo float a la que llamaremos rotateSpeedSmooth a la que igualaremos a un valor igual a 0,3 para indicar el intervalo del tiempo que queremos que tarde en realizar la transición. Ahora usaremos la función SmoothDampAngle para actualizar al valor correspondiente la variable currentRotation. Usaremos currentRotation para indicar la posición actual, targetRotation para indicar la posición objetivo, rotationV la variable de referencia para la velocidad actual y rotateSpeedSmooth para el tiempo que queremos que tarde en realizar la transición. La línea de código resultante será la siguiente:  currentRotation = Mathf.SmoothDampAngle (currentRotation, targetRotation, ref rotationV, rotateSpeedSmooth); . Para realizar la rotación modificaremos la variable eulerAngles de la clase Transform que indica la rotación del objeto en grados euler. Igualaremos su componente Y con la variable currentRotation y el resto a 0. Quedará por tanto transform.eulerAngles = new Vector3 (0, currentRotation, 0); . La función que usábamos anteriormente para realizar la rotación (transform.Rotate (0, Input.GetAxisRaw ("Horizontal") * rotateSpeed * Time.deltaTime, 0);) podemos borrarla o, si queremos mantenerla, convertirla en un comentario mediante el uso de doble barra (‘//’) al principio de la línea. Los comentarios son ignorados por Unity, su utilidad es simplemente informativa para el usuario que programa o aquel que va a leer el código.

rotationsmooth

Con estos pasos ya tendremos fluidez en el movimiento y rotación de nuestro personaje. El código de nuestro script hasta ahora es el siguiente:

using UnityEngine;
using System.Collections;

public class PlayerScript : MonoBehaviour {

public float moveSpeed = 5;
public float rotateSpeed = 180;
public float jumpSpeed = 20;
public float gravity = 9.8f;
public float moveSpeedSmooth = 0.3f;
public float rotateSpeedSmooth = 0.3f;

float currentForwardSpeed;
float forwardSpeedV;

float targetRotation;
float currentRotation;
float rotationV;

CharacterController controller;
Vector3 currentMovement;

void Start () {

controller = GetComponent ();
}

void Update () {

//transform.Rotate (0, Input.GetAxisRaw ("Horizontal") * rotateSpeed * Time.deltaTime, 0);

targetRotation += Input.GetAxisRaw ("Horizontal") * rotateSpeed * Time.deltaTime;
if (targetRotation > 360)
targetRotation -= 360;
if (targetRotation < 0)
targetRotation += 360;
currentRotation = Mathf.SmoothDampAngle (currentRotation, targetRotation, ref rotationV, rotateSpeedSmooth);
transform.eulerAngles = new Vector3 (0, currentRotation, 0);

currentForwardSpeed = Mathf.SmoothDamp (currentForwardSpeed, Input.GetAxisRaw ("Vertical") * moveSpeed, ref forwardSpeedV, moveSpeedSmooth);

currentMovement = new Vector3 (0, currentMovement.y, currentForwardSpeed);
currentMovement = transform.rotation * currentMovement;

if (!controller.isGrounded)
currentMovement -= new Vector3 (0, gravity * Time.deltaTime, 0);
else
currentMovement.y = 0;

if (controller.isGrounded && Input.GetButtonDown ("Jump"))
currentMovement.y = jumpSpeed;

controller.Move (currentMovement * Time.deltaTime);
}
}

Saltar y gravedad

En este post aprenderemos cómo hacer que el personaje salte y tenga gravedad.

Empezaremos por crear una variable pública de tipo float llamada jumpSpeed a la que asignaremos un valor igual a 20 que nos servirá para ajustar la altura del salto (public float jumpSpeed = 20;) y una variable pública de tipo float llamada gravity a la que asignaremos un valor igual a 9,8 (el valor de la gravedad terrestre) que indicará la gravedad que afecta a nuestro personaje, recordad poner la f para valores decimales (public float gravity = 9.8f;).

A continuación crearemos una sentencia if que aplique la gravedad a nuestro personaje cuando no esté tocando el suelo. Para ello comprobaremos el valor de la variable tipo bool isGrounded dentro de la clase CharacterController, que será true si el personaje está tocando el suelo. Como queremos aplicar la gravedad si no está en el suelo la sentencia if quedaría if (!controller.isGrounded) . El signo de exclamación ‘!’ indica negación. A la hora de aplicar la gravedad básicamente tendremos que restar a nuestro movimiento en el eje Y el valor de la variable gravity, de modo que quedaría currentMovement -= new Vector3 (0, gravity * Time.deltaTime, 0); . Cuando restamos o sumamos vectores entre sí los valores de cada componente afectan únicamente a su equivalente dentro de cada vector, es decir, eje X con eje X, Y con Y y Z con Z. Si realizamos una prueba con nuestro personaje levantándolo del suelo, veremos que la caída será muy lenta. Esto se debe a que en nuestro código el vector currentMovement se está igualando a 0 en el eje Y en cada frame. Para solucionar esto asignaremos al vector currentMovement el valor del eje Y que tenía en el frame anterior, para ello escribiremos el código currentMovement = new Vector3 (0, currentMovement.y, Input.GetAxis ("Vertical") * moveSpeed);.

code1gravity

Otra opción sería eliminar esta línea de código y modificar solamente la variable del eje Z de nuestro vector currentMovement que es la que afecta a nuestro movimiento (currentMovement.z = Input.GetAxis ("Vertical") * moveSpeed;). Además, para evitar una rotación infinita deberíamos igualar la variable del eje X a 0 (currentMovement.x = 0;).

code2gravity

Tenemos que tener en cuenta que con este código el valor del eje Y del vector currentMovement se decrementará cada vez que el personaje no esté tocando el suelo. Para evitar que este decremento se almacene e influya en la siguiente vez que el personaje no esté en el suelo, tenemos que igualar a 0 cada vez que entre en contacto con el suelo. Para ello recurriremos a la sentecia if-else de manera que si el objeto no está en contacto con el suelo le aplique la gravedad, pero si está en contacto con el suelo restablezca el valor del eje Y del vector currentMovement a 0. Para ello escribiremos a continuación del else lo siguiente: currentMovement.y = 0;.

ifelse

Para añadir la opción de salto realizaremos una sentencia if que compruebe que el personaje está en el suelo y que el usuario pulsa la tecla de salto, para ello el código sería if (controller.isGrounded && Input.GetButtonDown ("Jump")). A continuación escribiríamos el código currentMovement.y = jumpSpeed; que añadirá a la componente Y del vector currentMovement el valor de la variable jumpSpeed que provocará que nuestro personaje salte. Comprobaremos que a veces al darle a la barra espaciadora nuestro personaje no saltará. Esto se debe a que el juego toma los datos al inicio de cada frame y puede cuadrar que no recoja la información al presionar la tecla de salto. Esto lo corregiremos con unas pocas líneas de código posteriormente. Nuestro código final quedaría así:

using UnityEngine;
using System.Collections;

public class PlayerScript : MonoBehaviour {

public float moveSpeed = 5;
public float rotateSpeed = 180;
public float jumpSpeed = 20;
public float gravity = 9.8f;
CharacterController controller;
Vector3 currentMovement;

void Start () {

controller = GetComponent<CharacterController> ();
}

void Update () {

transform.Rotate (0, Input.GetAxis ("Horizontal") * rotateSpeed * Time.deltaTime, 0);

currentMovement = new Vector3 (0, currentMovement.y, Input.GetAxis ("Vertical") * moveSpeed);
currentMovement = transform.rotation * currentMovement;

if (!controller.isGrounded)
currentMovement -= new Vector3 (0, gravity * Time.deltaTime, 0);
else
currentMovement.y = 0;

if (controller.isGrounded && Input.GetButtonDown ("Jump"))
currentMovement.y = jumpSpeed;

controller.Move (currentMovement * Time.deltaTime);
}
}

Por último, crearemos un nivel de prueba para probar la funcionalidad del salto mediante el uso de varios cubos a distinta altura sobre los que saltar. Ajustaremos la gravedad y la variable jumpSpeed para configurar nuestro salto y la velocidad de caída. Y para organizar las cosas en Hierarchy crearemos un objeto vacío (Create->Create Empty) al que llamaremos Level y arrastraremos dentro de él los cubos, el suelo y la luz.

leveltestjump