Publicar un videojuego de Unity en WebGL

WebGL es una especificación estándar que está siendo desarrollada actualmente para mostrar gráficos en 3D en navegadores web. El WebGL permite mostrar gráficos en 3D acelerados por hardware (GPU) en páginas web, sin la necesidad de plug-ins en cualquier plataforma que soporte OpenGL 2.0 u OpenGL ES 2.0. Nos permitirá correr nuestro juego hecho en Unity en un navegador sin necesidad de instalar ningún plug-in.

WebGL genera un código basado en JavaScript, por lo que convertirá el código de nuestros scripts C# a JavaScript.

Para compilar nuestro juego en WebGL debemos seleccionarlo, en Build Settings, como plataforma a la que será exportado.

Build Settings

Cuando compilamos un proyecto WebGL, Unity creará una carpeta con los siguientes archivos:

  • un archivo index.html que incrusta el contenido en una página web.
  • un archivo JavaScript que contiene el código para el reproductor.
  • un archivo .mem que contiene la imagen binaria para inicializar la memoria heap para el reproductor.
  • un archivo .data que contiene los datos de assets y escenas.
  • algunos archivos JavaScript de ayuda para inicializar y cargar el reproductor.

directorio_1

directorio_2

Se puede visualizar el reproductor WebGL directamente en el navegador Firefox simplemente abriendo el archivo index.html. Por razones de seguridad, la mayoría de navegadores tendrán restricciones en scripts abiertos desde los archivos locales: URLs, así que esta técnica no funcionará. Si usamos Build & Run (File > Build & Run) el archivo será almacenado temporalmente en un servidor web local y abierto desde un host local URL (que evitará las restricciones).

Actualmente, FireFox y Chrome son los únicos navegadores con soporte Unity para WebGL.

Build Player Options

WebGL permite seleccionar una nivel de optimización en la ventana Build Settings. En general, «Slow» produce compilaciones no optimizadas, pero con tiempos de compilación mucho menores que el resto de opciones, de modo que puede ser usado para probar los posibles problemas del código. «Fast» produce compilaciones optimizadas, y «Fastest» permite algunas opciones adicionales de optimización, pero hace que los tiempos de compilación sean muy grandes (debería ser usado únicamente en lanzamientos finales).

Cuando marcamos la casilla «Development Build», Unity generará una compilación de desarrollo. Además, el los JavaScripts generados podrán ser leídos y mantienen los nombres de sus funciones, pero la compilación será demasiado grande para ser distribuida.

La casilla «Autoconnect Profiler» debe ser usada cuando queramos perfilar nuestro contenido Unity WebGL. No es posible conectar al Profiler a una compilación funcional como en otras plataformas, ya que las conexiones de profiler son mantenidas usando WebSockets en WebGL, pero el navegador sólo permitirá conexiones salientes desde el contenido, por lo que la única manera de usar el Profiler en WebGL es marcar «Autoconect Profiler» para tener el contenido del Editor.

Build Settings_Build Player Options

Player Settings

WebGL tienes algunas opciones adicionales en el Player Settings ( Edit > Project Settings > Player ).

La opción Strip Engine Code en Other Settings permite activar el código desnudo (code stripping) para WebGL. Nótese que WebGL no diferencia entre niveles de desnudo, activado o no. Si se activa, Unity no incluirá código para ninguna clase que no se use (si no se usa ningún componente o método de físicas, el motor de físicas completa será excluído de la compilación).

PlayerSettings_OtherSettings

El campo WebGL memory size en Publishing Settings permite especificar cuanta memoria (en MB) el contenido debería asignar para heap. Si el contenido es demasiado bajo, se obtendrán errores de memoria cuando el contenido sea cargado y la escena podría no caber en la memoria disponible. Pero si el valor es demasiado alto, el contenido podría fallar al cargarse en algunos navegadores o máquinas, porque el navegador podría no tener suficiente memoria disponible para asignar el tamaño del heap solicitado. Este valor es escrito para una variable llamada «TOTAL_MEMORY» en el archivo html generado, así que si queremos experimentar con esta opción, puede editarse en el archivo html para evitar recompilar el proyecto entero.

PlayerSettings_PublishingSettings

El desplegable Enable Exceptions en Publishing Settings nos permite activar la excepción de soporte en WebGL. Si no necesitamos ninguna excepción de soporte, establece esta opción a «None», lo cual dará el mejor rendimiento y las menores compilaciones. Cualquier excepción arrojada causará que el contenido se detenga con un error en esa opción. Pero si necesitamos usar excepciones, se pueden establecer las siguientes:

  • Explicitly Thrown Excaptions Only (default), permitirá capturar excepciones que son explícitamente lanzadas desde una sentencia «throw», y harán bloques «finally» en nuestro código de trabajo. Esto hará que el código JavaScript generado de nuestros scripts mayor y más lento, pero no es normalmente un problema si los scripts no son el principal cuello de botella de nuestro proyecto.
  • Full, que, además de lo anterior, también generará excepciones para referencias Null y para accesos fuera de los límites de arrays. Son generados por comprobaciones incrustadas para cualquier acceso a referencias en el código generado, por lo que causarán adicionalmente incrementos del tamaño del código y ralentizaciones. Además, añadirá rastro de pilas gestionadas para excepciones. Es aconsejable sólo usar este modo cuando necesitamos depurar errores en nuestro código, ya que genera compilaciones muy grandes y muy lentas.

La casilla Data caching en Publishing Settings permite activar el almacenamiento en caché local automático para los datos del reproductor. Si está activado, nuestros assets se almacenarán en un caché local en los navegadores en bases de datos IndexedDB, por lo que no tendrán que ser re-descargados en sucesivas ejecuciones del contenido. Nótese que diferentes navegadores tienen diferentes reglas en permitir almacenamiento IndexedDB, y podrían preguntar por permisos para almacenar los datos si está activado, y las compilaciones exceder el tamaño límite definido por el navegador.

Distribución de tamaño

Cuando publicamos para WebGL, es importante mantener el tamaño de nuestra compilación lo menos posible para evitar largos tiempos de descarga a los usuarios. Para consejos generales para reducir el tamaño de los assets, mirad aquí. Con respecto a WebGL:

  • No compiléis «Development Build», están diseñadas para preservar los nombres de las funciones y ocupan demasiado.
  • Estableced el nivel de optimización en «Fastest».
  • Estableced «Enable Exceptions» en Player Settings a «None».
  • Activad «Stripping Level» en Player Settings.
  • Tened cuidado cuando uséis third-party dlls, ya que pueden arrastrar muchas dependencias e incrementar el tamaño del código significativamente.

Si hacemos una compilación de lanzamiento, Unity generará una carpeta «Compressed», que contendrá una versión gzip-comprimido de la compilación. Si el servidor web está configurado correctamente, usará estos archivos en vez de los más grandes de la carpeta «Release», puesto que el protocolo http nativamente soporta compresiones gzip (más rápido que lo que las descompresiones que Unity podría mantener por sí mismo en código JavaScript). Sin embargo, esto significa que necesitamos asegurar que el servidor http provee los datos comprimidos. Podemos comprobar eso cargando el contenido e inspeccionando la red de transferencias en las herramientas de desarrollo de nuestros navegadores. Deberíamos encontrar «Content-Encoding: gzip» en la respuesta para el js y datos descargados. Si estamos hospedando nuestros archivos en un servidor web Apache, Unity escribirá un archivo invisible .htaccess en el directorio de la compilación, que dirá a Apache que comprima las transferencias, por lo que debería funcionar sin problemas. Para otros servidores web, comprobad los manuales de cada uno.

Release1_directorio

Instalar servidor de Apache

Para hospedar nuestro juego emplearemos un servidor Apache. Lo primero será descargarlo de http://httpd.apache.org/download.cgi#apache22 , pudiendo elegir entre varias versiones. En nuestro caso seleccionamos la última versión estable para Windows, 2.4.17. Nos llevará a la siguiente página: http://httpd.apache.org/docs/current/platform/windows.html#down, que mostrará toda la información que pudiéramos necesitar detallada. Ahí se nos mostrarán varias opciones de descarga, en mi caso he empleado Apache Lounge.

A continuación descomprimimos el archivo y copiamos la carpeta Apache24 en el disco C. Ahora ejecutamos en una ventada DOS con permisos de administrador (símbolo del sistema, por ejemplo) el  archivo httpd.exe de la carpeta bin. Luego instalamos el servicio mediante httpd.exe -k install.

console

Abriremos con doble clic ApacheMonitor.exe y nos mostrará los servicios instalados de Apache (Apache2.4 en nuestro caso). Seleccionándolo y dándole a Start se iniciará el servidor. Para comprobar que funciona podemos abrir un navegador y poner la dirección http://localhost/. Si todo funciona correctamente nos mostrará el mensaje «It works».

apachemonitor

Crearemos una carpeta a la que llamaremos «servidor_web» en C. Iremos al archivo de configuración httpd.conf de Apache en la carpeta conf. En la línea 149 Document Root especificará la carpeta en la que se encontrarán las páginas y los archivos a servir. Cambiaremos la ruta a la carpeta que hemos creado:

DocumentRoot «c:/servidor_web»
<Directory «c:/servidor_web»>

A continuación copiaremos nuestro juego exportado a WebGL a la carpeta c:/servidor_web. Si ahora vamos a http://localhost/ se ejecutará nuestro juego en WebGL.

Para acceder al servidor desde otra computadora conectada en una red local solo es necesario escribir en la barra de direcciones la dirección IP de la computadora que sirve de host, es decir la que tiene el servidor Apache instalado.

Para que tu conexión a internet pase a través del servidor será necesario configurarlo como un proxy fordward para eso en el archivo de configuración httpd.conf descomenta, (quitar el signo #) las siguientes líneas:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

Después agrega en el final del archivo la siguiente linea: ProxyRequests On
Cierra y guarda los cambios.

Accede a las Opciones de internet mediante el Panel de Control.

  • En la pestaña Conexiones pulsa el botón Configuración de LAN y marca la casilla Usar un servidor Proxy
  • Escribe en Dirección: 127.0.0.1 y en Puerto: 80 o el que vayas a usar.
  • Presiona Aceptar en todas las ventanas.
  • Reinicia el servidor.
  • Lo anterior se aplica si usas el navegador Internet Explorer y Google Chrome, si usas Firefox las opciones anteriores tienes que ingresarlas en:
    Opciones >Configuración >Configurar como Firefox se conecta a Internet.

A partir de ahora toda tu conexión pasa a través de Apache, sea direcciones locales o externas.

GUI 2D en juego 3D parte 2

Si hacemos clic en el botón se sombreará. Para modificar el tiempo que tarda en sombrearse modificamos la opción Fade Duration. Sin embargo, por mucho que pulsemos el botón no sucederá nada, ya que aún no hemos asignado nada a nuestro botón. La programación de la interacción del UI sigue una serie de pasos para cualquiera de sus elementos:

  1. Crear un objeto de la UI en la scene (por ejemplo nuestro botón).
  2. Escribir un script para llamar cuando la UI esté funcionando.
  3. Adjuntar ese script a un objeto de la scene.
  4. Unir los elementos de la UI al objeto con ese script.

Empezaremos creando un objeto controlador para unir al botón. Creamos un script llamado UIController y soltamos ese script en el objeto controlador en la escena. El código del script es el siguiente:

using UnityEngine;
using UnityEngine.UI;  // Importa el código del framework de UI
using System.Collections;

public class UIController : MonoBehaviour
{
[SerializeField] private Text scoreLabel;  // Hace referencia al objeto Text en la scene
                                           // para ajustar la propiedad del texto.
void Update()
{
scoreLabel.text = Time.realtimeSinceStartup.ToString();
}

public void OnOpenSettings() // Método llamado por el botón de ajustes
{
Debug.Log("open settings");
}
}

Ahora arrastramos el objeto Text de nuestro UI al hueco Score Label de nuestro script. Ahora mismo este texto será un contador debido al código del script.

texto_contador

A continuación añadimos una entrada OnClick al botón. Las entradas OnClick serán ejecutadas cuando se pulse el botón al que pertenecen. Cada entrada tendrá un hueco para el objeto y un menú para seleccionar la función a llamar; arrastramos el objeto controlador al hueco de la entrada y seleccionamos la función OnOpenSettings( ) de UIController. Si ahora ejecutamos el juego y pulsamos el botón se nos mostrará en Console el mensaje «open settings» (función OnOpenSettings( ) de UIController.cs).

entrada_onclick

Creamos ahora otra imagen para nuestro UI (GameObject>UI>Image) y le asignamos un sprite. Por defecto, la imagen asignada ocupará todo el espacio previamente asignado a esa objeto imagen. Para que tome sus valores originales le damos a la opción Set Native Size. La opción Image Type por defecto será Simple, pero puesto que esta imagen la usaremos como ventana pop-up la cambiaremos a Sliced. Nos aparecerá una advertencia: «This Image doesn’t have a border». En Project seleccionamos la imagen y hacemos clic en el botón Sprite Editor que aparece en Inspector y nos abrirá la ventada de Sprite Editor. Una imagen de tipo Sliced debe estar dividida en 9 secciones. Por lo tanto, en el Sprite Editor dividiremos la imagen en 9 secciones asignándole un valor de 12 píxeles a las cuatro líneas que aparecerán (L T R B). Ahora ya no aparecerá la advertencia y la imagen funcionará perfectamente. La redimensionaremos al tamaño que deseemos para la ventana pop-up (250 ancho, 200 alto y posición 0,0,0).

sprite editor

Ahora crearemos un script llamado SettingsPopup y lo añadiremos al objeto pop-up. El código del script será el siguiente:

using UnityEngine;
using System.Collections;

public class SettingsPopup : MonoBehaviour
{
public void Open()
{
gameObject.SetActive(true); // Activa el objeto para abrir la ventana.
}
public void Close()
{
gameObject.SetActive(false); // Desactiva el objeto para cerrar la ventana.
}
}

Lo siguiente será modificar el script UIController de este modo:

...

[SerializeField] private SettingsPopup settingsPopup;

void Start()
{
settingsPopup.Close (); // Cierra el pop-up cuando empieza el juego.
}

...

public void OnOpenSettings()
{
settingsPopup.Open (); // Reemplaza el texto Debug con el método de pop-up.
}

Se creará un nuevo hueco en UIController al que debemos arrastrar el objeto pop-up. Ahora el pop-up se cerrará al iniciar el juego y se abrirá cuando le demos al botón. Para cerrarlo crearemos un botón que situaremos en la esquina superior derecha del pop-up y en su texto pondremos «Cerrar» y le cambiaremos el color a blanco. Añadiremos una entrada OnClic y arrastraremos el objeto pop-up y seleccionaremos la funcion Close( ) para cerrarlo. Ahora cuando pulsemos este botón «Cerrar» el pop-up se cerrará.

Ahora crearemos un objeto Text, InputField y Slider para el UI; el texto será para identificar el slider. Todos estos objetos se crearán desde GameObject>UI. Todos serán hijos del objeto pop-up. En el texto del objeto Text pondremos «Velocidad» y en el de InputField>Placeholder escribiremos «Nombre» (este será el texto que el jugador ve antes de escribir nada). En las opciones del slider le pondremos de valor máximo 2 y marcaremos la opción Whole Numbers, de este modo no tomará decimales y se limitará a los valores [0 1 2] en este caso.

popup

Lo siguiente será escribir el código para los objetos creados. Añadiremos al script SettingsPopup lo siguiente:

...

public void OnSubmitName(string name) // Esto se lanzará cuando el usuario escriba su nombre.
{
Debug.Log(name);
}

public void OnSpeedValue(float speed) // Esto se lanzará cuando el usuario ajuste el slider.
{
Debug.Log("Speed: " + speed);
}

...

Seleccionando el inputfield podemos ver en Inspector un panel End Edit; los eventos que pongamos aquí se lanzarán cuando el jugador acabe de escribir. Añadimos una entrada, arrastramos el objeto pop-up al hueco y elegimos OnSubmitName( ) como función; la función será elegida en la sección de arriba (Dynamic strings) y no la de abajo (Static strings).

dynamic-string

Mismo proceso con el slider pero en el panel On Value Changed. Añadimos la entrada, arrastramos el objeto pop-up y elegimos OnSpeedValue( ) en la lista de valores dinámicos (sección superior). Si lanzamos el juego, introducimos un nombre en inputfield y pulsamos enter, se mostrará por Console ese nombre. Lo mismo si seleccionamos un valor en el slider.

Para alertar a la UI de las acciones de la scene, vamos a usar un broadcast messenger system. En la imagen se ve cómo funciona: los scripts pueden registrar para escuchar a un evento, otro código puede retransmitir un evento, y los listeners (los que «escuchan») serán alertados acerca de los mensajes de la retransmisión.

messenger system

El messenger system que usaremos lo encontramos en http://wiki.unity3d.com/index.php/CSharpMessenger_Extended. Crearemos un script llamado Messenger y guardaremos el código del enlace en él. También crearemos un script llamado GameEvent:

public static class GameEvent
{
public const string PRESS_BUTTON = "PRESS_BUTTON";
public const string SPEED_CHANGED = "SPEED_CHANGED";
}

Este script define una constante para un par de mensajes de evento; los mensajes están así más organizados y no necesitamos recordar y escribir el mensaje por todo el lugar.

Hasta ahora en el texto del UI se mostraba un contador para probar su funcionalidad. Pero ahora mostrará el número de veces que hacemos clic derecho con el ratón. Deberemos modificar UIController.cs para ello:

...

private int _score;

void Awake()
{                                                            // Declara qué metodo responde
Messenger.AddListener(GameEvent.PRESS_BUTTON, OnRightClick); // al evento PRESS_KEY
}
void OnDestroy()
{                                                              // Cuando un objeto es destruido,
Messenger.RemoveListener(GameEvent.PRESS_BUTTON, OnRightClick);// usamos un limpiador de listener
}                                                              // para evitar errores.

void Start()
{
_score = 0;                            // Inicializa el score a 0.
scoreLabel.text = _score.ToString ();

settingsPopup.Close ();
}

private void OnRightClick()
{
_score += 1;                           // Incrementa el score en respuesta al evento.
scoreLabel.text = _score.ToString();
}

...

El método Update( ) ha sido eliminado. Los métodos Awake( ) y OnDestroy( ) funcionarán como todo MonoBehaviour, como por ejemplo Start( ) o Update( ), de manera automática al crearse y destruirse el objeto. Un listener es añadido y eliminado en Awake( ) / OnDestroy( ). El listener es parte del broadcast messenger system, y llama a OnRightClick( ) cuando ese mensaje es recibido, que incrementará la puntuación y la mostrará en el marcador.

A continuación crearemos un script llamado IfRightClick. Este script simplemente comprobará si pulsamos el clic derecho del ratón y retransmitirá un mensaje de ser así. De este modo, cada vez que hagamos clic derecho en el juego, la puntuación de la esquina superior izquierda aumentará en 1. Este script lo añadiremos a nuestro personaje.

using UnityEngine;
using System.Collections;

public class IfRightClick : MonoBehaviour
{
void Update ()
{
if (Input.GetButtonDown ("Fire2"))
{
Messenger.Broadcast(GameEvent.PRESS_BUTTON);
}
}
}

Hemos visto cómo es recibido por el HUD un mensaje retransmitido por la scene. Veremos ahora el caso contrario, el HUD retransmite un mensaje a la scene. En nuestro ejemplo haremos que el slider sea un selector que indicará el número de veces que se llamará el evento PRESS_BUTTON si hacemos clic derecho con el ratón. Modificaremos IfRightClick.cs de la siguiente manera:

using UnityEngine;
using System.Collections;

public class IfRightClick : MonoBehaviour
{
private float speed;
int i;

void Awake()
{
Messenger<float>.AddListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}

void OnDestroy()
{
Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}

private void OnSpeedChanged(float value)
{
speed = value;
}

void Update ()
{
if (Input.GetButtonDown ("Fire2"))
{
for(i=(int)speed; i > 0; i--)
{
Messenger.Broadcast(GameEvent.PRESS_BUTTON);
}
}
}
}

Awake( ) y OnDestroy( ) añadirán y eliminarán, respectivamente, un event listener como en el caso anterior, pero los métodos tendrán un valor esta vez (OnSpeedChanged). También añadimos el método OnSpeedChanged que igualará el valor de speed al valor obtenido del slider (incialmente 0). Por último, hemos modificado Update( ) para que llame al evento PRESS_BUTTON las veces que indique la variable speed.

Finalmente en SettingsPopup.cs cambiaremos el contenido del método OnSpeedValue( ), añadiéndole un broadcast para enviar el mensaje del evento SPEED_CHANGED.

public void OnSpeedValue(float speed)
{
Messenger<float>.Broadcast(GameEvent.SPEED_CHANGED, speed);
}

Si ahora corremos el juego, dependiendo del valor del slider (0, 1 ó 2) el marcador se actualizará con cada clic que hagamos (añadiendo 0 si la posición del slider es 0, 1 para 1 y 2 para 2).

GUI 2D en juego 3D

En este post hablaremos de cómo construir una GUI 2D en un juego 3D en Unity. La GUI hace referencia a la parte visual de la interfaz, como texto y botones. Este texto y botones se superponen a la imagen del propio juego, creando una especia de display denominado HUD (heads-up display).

Desde su primera versión, Unity viene con un sistema de modo inmediato de GUI, el cual hace fácil poner un botón en la pantalla que podemos clicar. Este método está basado únicamente en código. Simplemente tendremos que añadir un script con el siguiente código a uno de los objetos de la Scene (funcionamiento GUI.Button):

using UnityEngine;
using System.Collections;

public class BasicUI : MonoBehaviour
{
void OnGUI()
{
if (GUI.Button (new Rect (10, 10, 40, 20), «Test»))
{
Debug.Log («Test button»);
}
}
}

boton_GUI_inmediato

El núcleo del código en este listado está en el método OnGUI( ). Al igual que con Start( ) y Update( ), MonoBehaviour responde automáticamente a OnGUI( ). La función será llamada cada frame después de todo el renderizado. Este código dibujará un botón que responderá al hacer clic en él, devolviendo un mensaje en Console: «Test button». Con el modo inmediato es fácil crear botones en pantalla, sin embargo para todo lo demás (e incluso para botones) lo mejor es usar el método avanzado introducido en las versiones 4.6+ de Unity empleado desde el Editor. Esta UI trabajará en modo retenido, de modo que los gráficos son trazados una vez y dibujados entonces cada frame sin necesidad de estar continuamente redefiniéndolos. Al trabajar con este sistema desde el Editor tendremos dos ventajas sobre el modo inmediato: 1)podemos ver la apariencia que tendrá nuestra UI y 2)la UI puede ser personalizado con nuestras propias imágenes.

Importaremos ahora las imágenes que vayamos a usar en nuestra UI y cambiaremos su tipo de textura a Sprite (2D and UI).

sprite_type

Para crear nuestra UI tendremos que crear un Canvas, una especie de objeto especial que Unity usa como la UI del juego. Para ello vamos a GameObject>UI>Canvas. El objeto Canvas (que renombraremos como HUD) representa la extensión completa de la pantalla, y será muy grande debido a que escala con respecto a los píxeles de la pantalla (un pixel de la pantalla = un punto en la Scene). Al crear el Canvas también se creará un objeto EventSystem, requerido para la interacción del UI pero que podemos ignorar. En el ajuste de Render Mode del Canvas tendremos tres opciones:

  • Screen Space-Overlay: Renderiza la UI como gráficos 2D en la vista de la cámara (opción por defecto).
  • Screen Space-Camera: también renderiza la UI en la vista de la cámara, pero permite la rotación de sus elementos.
  • World Space: ubica el objeto Canvas en la Scene como si fuese parte de ella.

Marcando la opción de Pixel Perfect se ajustará la posición de las imágenes de manera automática.

Crearemos ahora una imagen, un texto y un botón para nuestro Canvas desde GameObject>UI. Estos elementos de nuestra UI tienen que ser hijos del Canvas (Unity lo hará por defecto). El botón tendrá un elemento de texto que eliminaremos para nuestro ejemplo. Colocaremos de manera aproximada por ahora la imagen en la esquina superior izquierda, seguida por el texto, y en la esquina superior derecha el botón.

canvas_posicion_aprox

En la imagen podemos seleccionar la imagen a usar para este elemento abriendo la ventana con los sprites disponibles en nuestro proyecto dándole al botón que aparece a la derecha del recuadro de Source Image o bien arrastrando a ese mismo recuadro el sprite que deseemos asignarle.

canvas_seleccionar_img

Si seleccionamos la opción Set Native Size la imagen tomará las medidas originales del sprite. Del mismo modo asignamos otro sprite al botón.

Pasemos ahora al texto. En el recuadro Text escribiremos el texto que queremos que muestre («Texto de prueba», por ejemplo). Podremos modificar la fuente de la letra, así como su tamaño y estilo. También podemos seleccionar el tipo de alineación que tendrá el texto dentro del recuadro que lo contiene.

canvas_texto

Por último, miraremos cómo controlar la posición de los elementos de la UI. Todos los objetos de la UI tienen un anchor (ancla), mostrado en el Editor como objetivo X. El anchor de un objeto es el punto donde el objeto se une al canvas o a la pantalla, determina con respecto a qué es medida su posición. El anchor es definido como el «centro de la pantalla» y el anchor se mantendrá centrado aun cuando la pantalla cambie de tamaño. Si, por ejemplo, ajustamos el canvas al borde derecho de la pantalla, el objeto se mantendrá fijo al borde derecho aun si se cambia el tamaño de la pantalla. Por defecto, el anchor se situará en el centro de la pantalla, pero podemos modificarlo manualmente o ajustándolo a uno de los valores de los presets.

anchor

Los anchor points pueden ajustar tanto la posición como la escala (si se sitúan más allá de la posición del objeto).

anchor_escala1 anchor_escala2

Usando Git con Unity

En este post aprenderemos a usar Git con Unity para mantener un control de versiones de nuestros proyectos.

Debemos decidir cuál será nuestro flujo de trabajo, el cómo trabajaremos. Esto será especialmente importante si están varias personas trabajando en un mismo proyecto. Un buen flujo de trabajo sería el denominado Git Flow.

Tendremos también que elegir una Git GUI (interfaz gráfica de usuario) en caso de que deseemos utilizar alguna. Una opción altamente recomendada sería SourceTree. Podéis descargarla desde aquí y tenéis también un tutorial de uso. A mayores una vez instaléis la aplicación tendréis una pequeña guía paso a paso dentro del propio programa que se os mostrará la primera vez que lo ejecutéis.

Además habrá que crear un archivo .gitignore en nuestro directorio del proyecto de Unity que definirá los archivos y directorios que no se suben al repositorio de Git. Crearemos un archivo nuevo de texto que llamaremos gitignore y le agregaremos el siguiente contenido:

# =============== #
# Unity generated #
# =============== #
Temp/
Library/

# ===================================== #
# Visual Studio / MonoDevelop generated #
# ===================================== #
ExportedObj/
obj/
*.svd
*.userprefs
/*.csproj
*.pidb
*.suo
/*.sln
*.user
*.unityproj
*.booproj

# ============ #
# OS generated #
# ============ #
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

Ahora seleccionaremos la opción «Guardar como» y marcaremos la opción «todos los archivos». Le pondremos por nombre .gitignore y lo guardamos.

Por último nos queda configurar Unity. Vamos a Edit->Project Settings->Editor y ahí modificaremos dos opciones. La primera será cambiando en Version Control Mode el valor a Visible Meta Files. La segunda será cambiando en Asset Serialization Mode el valor a Force Text.

Unity Test Tools

En este post aprenderemos los conceptos básicos de cómo utilizar los assets de testeo de Unity. Una de las partes positivas de esta herramienta es que nos permite realizar los tests sin necesidad de tener un profundo conocimiento del código. Podéis encontrar Unity Test Tools en el assets store (https://www.assetstore.unity3d.com/en/#!/content/13802) e integrarlo en cualquier proyecto existente de Unity. Desde Unity tendréis que ir a Window->Asset Store.

utt

Se os abrirá una ventana con el Asset Store de Unity. En el buscador tendréis que poner el nombre de la herramienta «Unity Test Tools» y una vez dentro de él haced clic en el botón «Download» o «Import» si ya lo tenéis descargado. A continuación se os abrirá otra ventana para importar el asset a vuestro proyecto.

utt2

Ahora tendremos una carpeta en Project con el nombre UnityTestTools. Esta carpeta deberá ser eliminada, así como los componentes de este asset empleados, antes de exportar nuestro juego, ya que su función es únicamente de testeo. No obstante, según la documentación todo lo relacionado con el asset es desactivado cuando compilamos el juego. En dicha carpeta tendremos un enlace con toda la documentación de esta herramienta.

El primer punto que trataremos será el Assertion component. Este componente nos permitirá asegurar si algo es verdadero. Una vez importado el asset Unity Test Tools, este componente se podrá añadir a cualquier objeto en Add Components->Scripts->UnityTest->Assertion Component. Una vez configurado nos mostrará un error en caso de que la afirmación que hemos puesto no se cumpliese. Para facilitar el saber cuándo se produce dicho error podríamos activar Error Pause en Console. Esta opción pausará el juego en el momento en el que aparezca un error en la consola. Por ejemplo, imaginad que queremos asegurar que nuestro personaje se mantendrá siempre en una posición en el eje Y inferior a un valor igual a 4. En la siguiente imagen podéis observar la configuración pertinente para realizar la comprobación. Compararemos una variable tipo float en cada ciclo de Update con una frecuencia de 1 segundo. Dicha variable hará referencia al componente Transform.position.y del objeto Player y comprobará si es menor que un valor constante igual a 4.

assertion_component_1

Si ahora corremos nuestro juego y superamos esa altura con nuestro personaje, el juego se pausará y la consola mostrará un error advirtiendo de que el valor de Player.Transform.position.y ha superado el valor constante de 4 que habíamos marcado como límite.

assertion_component_2

En la documentación viene detallado el uso de Assertion Component (https://bitbucket.org/Unity-Technologies/unitytesttools/wiki/AssertionComponent) y en internet podréis encontrar más información todavía. Este complemento nos permite realizar pruebas sin necesidad de introducir código nuevo dentro de nuestros scripts que posteriormente tendríamos que eliminar.

El siguiente apartado será Integration Test. Este tipo de test comprobará que los objetos interaccionan entre ellos del modo que nosotros esperamos. Lo primero que debemos comprender es que los Integration Tests no se encontrarán en las Scenes de nuestro juego, sino que tendrán su propio «mundo». Cada Integration Test tendrá su propia Scene. Una vez creada una nueva Scene iremos al menú Unity Test Tools->Integration Test Runner (este menú se creará al importar el asset Unity Test Tools). Aparecerá una ventana nueva llamada Integration Tes que podremos integrar al igual que las ventanas Hierarchy, Inspector, Project, etc.

integration_test_runner

Crearemos un nuevo test con el botón Create. Una vez creado tendremos varias opciones. Seleccionaremos un nombre, las plataformas (windows, android, etc) incluídas en el test, cuanto tiempo debe durar (si dura más se considerará fallido), si debe ser ignorado, si deben ser ejecutadas todas las assertions o si habrá alguna excepción. Además se crearán dos objetos: un test runner y otro con el nombre de nuestro test (donde editamos las opciones anteriormente mencionadas).

integration_test_runner_2

Ahora crearemos dentro de nuestro test una esfera en la posición (0, 1.5, 0) y le añadiremos un rigidbody al que le cambiaremos el valor Drag a 20. Crearemos también un plano a partir de un cubo en la posición (0, 0, 0) al que le marcaremos la opción Is Trigger. Procederemos a continuación con un test muy sencillo. Si en un intervalo de tiempo de 3 segundos la esfera no impacta con el plano se superará el test, en caso contrario se considerará como fallido. Para ello crearemos dos Call Testing mediante Add Componet->Scripts->UnityTest->Call Testing. En el primero pondremos la condición para que el test sea un éxito. Le pondremos un valor de 3 segudos y que se dé como válido el test al cumplirse ese número de segundos (Call after seconds). En el segundo pondremos que si algo atraviesa (On trigger enter) el plano se de el test por fallido. Con estas condiciones si ejecutamos el test con clic derecho->Run se superará con éxito y pondrá un signo verde de aprobación al lado de su nombre.

test_pass

Si ahora modificamos el valor de Drag a 0 y ejecutamos el test, podremos observar que ahora el resultado será fallido y tendremos una marca roja en lugar de la verde para indicar que se ha fallado ese test.

test_fail

Existe la opción de realizar los test mediante un script que añadimos en el objeto del test. Esto nos da la posibilidad de realizar test realmente complejos de ser necesarios. Si en el script incluímos la línea IntegrationTest.Pass(gameObject); se dará el test por válido, mientras que con IntegrationTest.Pass(gameObject); se dará por fallido.

Crearemos otro integration test de ejemplo y dentro de él crearemos dos esferas a las que le añadiremos el componente Rigidbody. Si os fijáis, cuando tengáis seleccionado uno de los tests el resto de ellos no serán visibles en la escena. Debajo de la primera esfera crearemos un cubo en el que marcaremos la opción Is Trigger y le añadiremos el siguiente script llamado PowerUp:

using UnityEngine;
using System.Collections;

public class PowerUp : MonoBehaviour {

void OnTriggerEnter (Collider other)
{
var scale = other.transform.localScale;

other.transform.localScale = scale * 2;
}
}

Este script lo que hará es duplicar el tamaño de la primera esfera cuando atraviese al cubo. Ahora le añadiremos al cubo script de Assertion Component que comparará variables tipo float cuando la primera esfera entre en el cubo y se asegurará de si la componente Transform.localScale.x de esa esfera es mayor que la misma componente de la segunda esfera. Puesto que el script PowerUp duplicará el tamaño de la primera esfera cuando atraviese al cubo, el Assertion Component se debería cumplir.

assertion_integration_test_1

Iremos ahora al objeto del test y marcaremos la opción Succeed on assertion. De este modo el test será superado con éxito si se cumplen todas las assertions, mientras que será considerado fallido en el otro caso. Si ahora corremos el segundo test debería dar un resultado exitoso. Si le damos al botón Run All de la ventana de Integration Tes se ejecutarán todos los test que tengamos uno detrás de otro.

assertion_integration_test_2

Podemos incluír tantos test como queramos. Todos ellos se ejecutarán de manera independiente dando los resultados correspondientes de cada uno de ellos. Podéis comprobar los ejemplos incluidos en el asset Unity Test Tools que hay en la carpeta Examples.

El último apartado que trataremos será Unit Test. Este será como un Integration Test pero para probar nuestro código. Empezaremos yendo a Unity Test Tools->Unit Test Runner. Se nos abrirá la ventana Unit Test que podremos poner donde mejor consideremos al igual que hicimos con la ventana Integration Tes. Dentro de esta ventana existen diversos ejemplos ya hechos. Para crear nuestros propios Unit Tests necesitamos aprender NUnit. En la documentación existe más información al respecto (https://bitbucket.org/Unity-Technologies/unitytesttools/wiki/UnitTestsRunner).

unit_test

Os mostraré un ejemplo muy simple a continuación. Crearemos una carpeta a la que llamaremos Editor y dentro un Script al que llamaremos SampleTest. En dicho script borraremos todo y escribiremos el siguiente código:

using System;
using NUnit.Framework;

public class SampleTest
{
[Test]
public void SimpleAddition()
{
Assert.That (2 + 2 == 4);
}
}

Básicamente este test comprobará que 2 + 2 = 4. Obviamente será superado con éxito, pero lo comprobaremos. Creamos una escena nueva. En nuestra ventana Unit Tests tendremos ahora el que acabamos de crear, SampleTest. Si lo ejecutamos nos dirá que ha pasado favorablemente la prueba.

unit_test_2

Con esto acaban las nociones básicas del uso del asset Unity Test Tools, una herramienta muy útil para realizar cualquier tipo de prueba a nuestro juego de una manera más visual sin entrar tanto en el propio código. Recordad recurrir a la documentación para más información (https://bitbucket.org/Unity-Technologies/unitytesttools/wiki/Home).

Controles avanzados de salto

En este post aprenderemos un poco más acerca del salto de nuestro personaje.

Empezaremos hablando del control de nuestro personaje cuando está en el aire. Ahora mismo el movimiento de nuestro personaje cuando está en el aire es igual que cuando está en el suelo. Lo que haremos será que el personaje reduzca su capacidad de movimiento cuando no esté en el suelo. Para ello modificaremos la siguiente línea de código:

currentMovement = Vector3.SmoothDamp(currentMovement, targetHorizontalMovement * moveSpeed, ref currentMovementV, moveSpeedSmooth);

Sustituiremos la variable moveSpeedSmooth, que indicará el tiempo que el personaje tarda en alcanzar su velocidad máxima de movimiento, por otra cuyo valor dependerá de si el personaje está o no en el suelo. Esta variable de selección será privada de tipo float y la llamaremos moveSmoothUse ( float moveSmoothUse; ). Le asignaremos el valor de moveSpeedSmooth dentro de Start() para cuando se inicialice del siguiente modo: moveSmoothUse = moveSpeedSmooth; . Crearemos una variable pública de tipo float a la que llamaremos airControlSmooth y a la que le asignaremos un valor de 0.8 ( public float airControlSmooth = 0.8f; ). Esta variable es la que seleccionaremos en caso de que nuestro personaje esté en el aire. Ahora sustituímos moveSpeedSmooth por moveSmoothUse:

currentMovement = Vector3.SmoothDamp(currentMovement, targetHorizontalMovement * moveSpeed, ref currentMovementV, moveSmoothUse);

A continuación iremos a la parte donde detectamos si el personaje está o no en el suelo. Si está tocando el suelo igualaremos moveSmoothUse con moveSpeedSmooth y si no lo está la igualaremos a airControlSmooth.

if (!controller.isGrounded)
{
moveSmoothUse = airControlSmooth;
verticalSpeed -= gravity * Time.deltaTime;
}
else
{
moveSmoothUse = moveSpeedSmooth;
verticalSpeed = 0;
}

El siguiente punto a tratar será introducir un pequeño margen de tiempo para poder saltar, de modo que el personaje pueda saltar aunque ya no esté tocando el suelo, siempre que se encuentre dentro de ese margen temporal. Con esta finalizad crearemos una variable pública tipo float a la que llamaremos jumpAllowTime que igualaremos a 0.1 ( public float jumpAllowTime = 0.1f; ). Esta variable indicará el margen de tiempo en segundos para saltar si previamente se estaba tocando el suelo, aunque ahora esté en el aire. Ahora crearemos un contador para saber si el personaje se encuentra dentro de ese intervalo. Será  una variable privada de tipo float a la que llamaremos jumpAllowTimeTrackfloat jumpAllowTimeTrack; ) y la inicializaremos dentro de Start() igualándola a jumpAllowTime (  jumpAllowTimeTrack = jumpAllowTime; ). Cuando el personaje esté en el suelo jumpAllowTimeTrack tomará el valor de jumpAllowTime, mientras que si está en el aire decrementaremos el valor de jumpAllowTimeTrack con respecto al tiempo:

if (!controller.isGrounded)
{
moveSmoothUse = airControlSmooth;
verticalSpeed -= gravity * Time.deltaTime;
jumpAllowTimeTrack -= Time.deltaTime;
}
else
{
moveSmoothUse = moveSpeedSmooth;
verticalSpeed = 0;

jumpAllowTimeTrack = jumpAllowTime;
}

Ahora modificaremos las condiciones de salto. Cambiaremos la comprobación de si el personaje está en el suelo por una comprobación de que el contador aún no ha llegado a 0.

if (jumpAllowTimeTrack >= 0 && Input.GetButtonDown ("Jump"))
verticalSpeed = jumpSpeed;

El siguiente tema a tratar es que a veces al pulsar la barra espaciadora para saltar el personaje no realizará dicha acción. Esto se debe a que puede darse el caso de que pulsemos justo entre un frame y otro y el juego no recogerá la orden. A simple vista parece que ya está solucionado (en mi caso al menos), pero explicaremos como solventar este problema. Básicamente tendremos que hacer lo mismo que en el apartado anterior pero a la inversa. Crear dos variables waitToLand y waitToLandTrack:

public float waitToLand = 0.1f;
float waitToLandTrack;

Ahora será hacer el mismo proceso pero cambiado de posición respecto a la comprobación de suelo. Además tendremos que igualar verticalSpeed a 0 sólo si waitToLandTrack es menor o igual a 0.

if (!controller.isGrounded)
{
moveSmoothUse = airControlSmooth;
verticalSpeed -= gravity * Time.deltaTime;
jumpAllowTimeTrack -= Time.deltaTime;
waitToLandTrack = waitToLand;
}
else
{
moveSmoothUse = moveSpeedSmooth;
jumpAllowTimeTrack = jumpAllowTime;
waitToLandTrack -= Time.deltaTime
}
if(waitToLandTrack <= 0)
verticalSpeed = 0;

A partir de ahora el personaje saltará siempre que pulsemos la barra espaciadora.

Recordad activar el script de la cámara para que vuelva a seguir a nuestro personaje y ajustad los valores de las variables a vuestro gusto para conseguir la fluidez según vuestras preferencias.

El código del script PlayerScript.cs quedará así:

using UnityEngine;
using System.Collections;


public class PlayerScript : MonoBehaviour {

public float moveSpeed = 5;
public float rotateSpeed = 180;
public float jumpSpeed = 20;
public float jumpAllowTime = 0.1f;
float jumpAllowTimeTrack;
public float waitToLand = 0.1f;
float waitToLandTrack;
public float gravity = 9.8f;
public float moveSpeedSmooth = 0.3f;
public float airControlSmooth = 0.8f;
public float rotateSpeedSmooth = 0.3f;

float moveSmoothUse;

float currentForwardSpeed;
float forwardSpeedV;

float targetRotation;
float currentRotation;
float rotationV;

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

void Start () {

jumpAllowTimeTrack = jumpAllowTime;
waitToLandTrack = waitToLand;
moveSmoothUse = moveSpeedSmooth;
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;
currentMovement = Vector3.SmoothDamp(currentMovement, targetHorizontalMovement * moveSpeed, ref currentMovementV, moveSmoothUse);

//Quaternion targetRotationQ = Quaternion.LookRotation(Vector3.forward);
if (new Vector3(currentMovement.x, 0, currentMovement.z).magnitude > 1)
{
targetRotation = Mathf.Atan2(currentMovement.x, currentMovement.z) * Mathf.Rad2Deg;
//transform.rotation = Quaternion.Lerp(transform.rotation, targetRotationQ, rotateSpeed * Time.deltaTime);
transform.rotation = Quaternion.Euler(0, Mathf.SmoothDampAngle(transform.rotation.eulerAngles.y, targetRotation, ref rotationV, rotateSpeedSmooth), 0);
}

if (!controller.isGrounded)
{
moveSmoothUse = airControlSmooth;
verticalSpeed -= gravity * Time.deltaTime;
jumpAllowTimeTrack -= Time.deltaTime;
waitToLandTrack = waitToLand;
}
else
{
moveSmoothUse = moveSpeedSmooth;
jumpAllowTimeTrack = jumpAllowTime;
waitToLandTrack -= Time.deltaTime;
}
if(waitToLandTrack <= 0)
verticalSpeed = 0;
if (jumpAllowTimeTrack >= 0 && Input.GetButtonDown ("Jump"))
verticalSpeed = jumpSpeed;

currentMovement.y = verticalSpeed;

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

Rotación del jugador y trigonometría

En este post aprenderemos a hacer que nuestro personaje rote de manera que mire en la dirección en la que avanza. Para ello existen diversos métodos.

Empezaremos empleando la función LookRotation de la clase Quaternion, que lo que hará es rotar con respecto al Vector3 que le pasemos como parámetro, devolviendo el Quaternion resultante. El Vector3 que le pasaremos será currentMovement, así que lo que hará LookRotation es rotar a nuestro personaje de manera que quede mirando en el sentido de la dirección del movimiento de dicho vector. Para ello el Quaternion generado lo almacenaremos en transform.rotation para modificar la rotación del personaje. Sin embargo, observaremos que si le pasamos el vector currentMovement entero nuestro personaje se empezará a balancear cuando esté quieto. Esto lo evitaremos igualando a cero la componente Y del vector currentMovement. El código nuevo lo situaremos depués de la línea

currentMovement = Vector3.SmoothDamp(currentMovement, targetHorizontalMovement * moveSpeed, ref currentMovementV, moveSpeedSmooth);

El código será el siguiente:

transform.rotation = Quaternion.LookRotation(new Vector3(currentMovement.x, 0, currentMovement.z));

Si no existiese movimiento, currentMovement = (0, 0, 0) , LookRotation provocará que nuestro personaje mire al frente sin importar cual es su rotación actual. Para evitar esto podríamos usar una sentencia if que cumpruebe que currentMovement no sea Vector3.zero. El problema es que cuando los valores se aproximan mucho a 0 unity asumirá que son 0, por lo tanto debemos comprobar que la magnitud del vector sea mayor que un número próximo a 0 como puede ser 1. Así pues lo que haremos será una sentencia if que compruebe que el vector que usamos con LookRotation (recordad que suprimimos la componente y de currentMovement) tenga una magnitud superior a 1. El código quedaría así:

if(new Vector3(currentMovement.x, 0, currentMovement.z).magnitude > 1)
transform.rotation = Quaternion.LookRotation(new Vector3(currentMovement.x, 0, currentMovement.z));

A continuación haremos que los giros del personaje no sean tan bruscos usando la función Lerp de la clase Quaternion (si no recordáis cómo se usaba repasad los tutoriales pasados o mirad en ScriptReference). Para ello crearemos una variable Quaternion targetRotationQ donde almacenaremos la rotación que nos devuelve LookRotation. Después haremos que transform.rotation cambie su valor al de targetRotationQ a la velocidad indicada por rotateSpeed (ajustadla a un valor igual a 10, por ejemplo) usando la función Lerp. El código será:

Quaternion targetRotationQ = Quaternion.LookRotation(Vector3.forward);
if (new Vector3(currentMovement.x, 0, currentMovement.z).magnitude > 1)
{
targetRotationQ = Quaternion.LookRotation(new Vector3(currentMovement.x, 0, currentMovement.z));
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotationQ, rotateSpeed * Time.deltaTime);
}

Con lo hecho hasta aquí ya haremos que nuestro personaje avance de cara en el sentido de su movimiento. Sin embargo, en el futuro vamos a necesitar hacer uso de la función SmoothDampAngle de la clase Mathf, por lo que lo dejaremos explicado en este post. Sustituiremos la línea de código de Lerp por la que haremos a continuación.

La función SmoothDampAngle actuará del mismo modo que SmoothDamp, con la salvedad de que al estar preparado para operar con valores de ángulos ya estará limitado a los valores posibles (0-360º) para este caso. Nuestro personaje únicamente rotará en el eje Y por lo tanto sólo modificaremos la rotación de Y.

rotationY

Debemos tener en cuenta que SmoothDampAngle trabaja con variables tipo float y devuelve otra variable tipo float. Por tanto para obtener el valor del ángulo de un Quaternion usaremos la función eulerAngles.

Mathf.SmoothDampAngle(transform.rotation.eulerAngles.y, targetRotationQ.eulerAngles.y, ref rotationV, rotateSpeedSmooth)

Además transformaremos el valor que devuelve SmoothDampAngle a un Quaternion usando la función Euler de la clase Quaternion. El código resultante final para SmoothDampAngle será:

transform.rotation = Quaternion.Euler(0, Mathf.SmoothDampAngle(transform.rotation.eulerAngles.y, targetRotationQ.eulerAngles.y, ref rotationV, rotateSpeedSmooth), 0);

Con eso finaliza la parte de rotación del personaje. Lo que viene a continuación serán algunos conceptos trigonométricos. Las funciones las encontraremos dentro de la clase Mathf, por ejemplo Sin (seno), Cos (coseno) y Tan (tangente). Estas funciones pueden ser usadas para resolver los ángulos de un triángulo rectángulo. Concretamente a nosotros nos interesará Tan (tangente) y Atan (arcotangente).

tan

En la imagen vemos un triángulo rectángulo y la relación que existe con la tangente con respecto a uno de los ángulos que no son rectos (90º) y los lados que forman el ángulo recto, que son conocidos como catetos y están marcados con la letra A y O. Si colocásemos a nuestro personaje en el vértice del ángulo que está marcado con la letra G, la letra A (cateto contiguo) se corresponderia con la componente z de nuestro vector currentMovement, la letra O (cateto opuesto) sería la componente x y la G (el ángulo) indicaría la rotación del eje y.

Lo que haremos ahora es hacer que nuestro personaje haga la rotación mediante el uso de la función Atan, ya que conocemos currentMovement.x (O) y currentMovement.z (A), para obtener el valor de rotación (G) de nuestro eje y. Debemos tener cuidado, ya que la función Atan devolverá un valor de rotación en radianes. Tendremos que hacer la conversión de radianes a grados. Para ello usaremos la función Rad2Deg. Ya que Atan devolverá un valor tipo float sustituiremos targetRotationQ, que era un Quaternion, por targetRotation, que es una variable tipo float. El código en el que usaremos la función Atan sustituirá a la línea de LookRotation y puesto que no vamos a emplear targetRotationQ la dejaremos como comentario o la eliminaremos:

//Quaternion targetRotationQ = Quaternion.LookRotation(Vector3.forward);
if (new Vector3(currentMovement.x, 0, currentMovement.z).magnitude > 1)
{
targetRotation = Mathf.Atan(currentMovement.x / currentMovement.z) * Mathf.Rad2Deg;
//transform.rotation = Quaternion.Lerp(transform.rotation, targetRotationQ, rotateSpeed * Time.deltaTime);
transform.rotation = Quaternion.Euler(0, Mathf.SmoothDampAngle(transform.rotation.eulerAngles.y, targetRotation, ref rotationV, rotateSpeedSmooth), 0);
}

Se nos presentará un problema cuando el personaje se mueva en sentido negativo en el eje z con respecto a la posición de la cámara y es que no se orientará hacia el sentido del movimiento sino todo lo contrario. Necesitaremos usar la función Atan2, que es como Atan con la salvedad de que opera en toda la circunferencia incluyendo los valores en los que currentMovement.z pueda ser 0. Modificaremos la línea de Atan del siguiente modo:

targetRotation = Mathf.Atan2(currentMovement.x, currentMovement.z) * Mathf.Rad2Deg;

Con esto se solucionará el problema y el personaje se orientará siempre en el sentido del movimiento del vector currentMovement.

A continuación dejo una captura de cómo quedará el código realizado a lo largo de este post (nótese que el editor no es MonoDevelop, sino Microsoft Visual Studio, pero a efectos prácticas funcionará igual).

code_atan2

Aquí os dejo escrito todo el código que nos queda en PlayerScript.cs :

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;
Vector3 currentMovementV;
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;
currentMovement = Vector3.SmoothDamp(currentMovement, targetHorizontalMovement * moveSpeed, ref currentMovementV, moveSpeedSmooth);

//Quaternion targetRotationQ = Quaternion.LookRotation(Vector3.forward);
if (new Vector3(currentMovement.x, 0, currentMovement.z).magnitude > 1)
{
targetRotation = Mathf.Atan2(currentMovement.x, currentMovement.z) * Mathf.Rad2Deg;
//transform.rotation = Quaternion.Lerp(transform.rotation, targetRotationQ, rotateSpeed * Time.deltaTime);
transform.rotation = Quaternion.Euler(0, Mathf.SmoothDampAngle(transform.rotation.eulerAngles.y, targetRotation, ref rotationV, rotateSpeedSmooth), 0);
}

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);
}
}

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

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);
}

}