Git Flow

En este post se explicará el flujo de trabajo Git Flow que aplicaremos cuando usemos Git al trabajar en los proyectos con Unity. Este modelo de desarrollo que se mostrará a continuación no es más que un conjunto de procedimientos que todo miembro del equipo debe seguir en el proceso de desarrollo de un proyecto.

Empezaremos por establecer un sistema de reposiciones en el que consideraremos un repositorio central al que nos referiremos como origin. Cada desarrollador realiza operaciones de push y pull hacia origin. De igual forma puede realizar push y pull hacia otros miembros dentro de su subequipo. De este modo los miembros de ese subequipo pueden trabajar juntos sobre algo concreto antes de subirlo prematuramente a origin. Básicamente los miembros de ese subequipo se definen como Git remotos entre sí. En la siguiente imagen se describen tres subequipos (bob-alice, alice-david, david-clair) conectados entre sí además de con origin.

origin

El repositorio central tendrá dos ramas principales con tiempo de vida infinito:main-branches

  • master
  • develop

En la rama origin/master HEAD siempre estará en la última versión estable de nuestro código fuente. Por otra parte, en la rama origin/develop tendrá una versión de desarrollo con los últimos cambios de la próxima versión estable. Cuando el código fuente de develop alcanza un punto estable, estos cambios serán añadidos al código fuente en master y etiquetados con un número de versión. Se creará por lo tanto una nueva última versión en la rama master.

Junto con las ramas principales master y develop, se usarán ramas de apoyo para ayudar al desarrollo paralelo entre los miembros de equipo, facilitar el seguimiento de características, prepararse para nuevos lanzamientos y ayudar a solucionar rápidamente problemas de producción en vivo. Estas ramas de apoyo tendrán un tiempo de vida limitado y serán eliminadas llegado el momento.

Los diferentes tipos de ramas de apoyo que podríamos usar son:

feature_branch

  • Ramas de característica

Puede ramificarse o unirse a develop. Pueden tener cualquier nombre salvo master, develop, release-* o hotfix-*. Son usadas para el desarrollo de nuevas características de versiones futuras. A la hora de desarrollar una nueva característica podría no saberse la versión objetivo a la que será incorporada. Esta rama existirá mientras dure su desarrollo; hasta que se una a develop o sea descartada. Este tipo de ramas suele existir sólo en develop y no en origin.

Cuando empecemos a trabajar en una nueva característica se ramificará desde la rama develop.

git checkout -b myfeature develop
Cambiado a una nueva rama "myfeature"

Las características finalizadas podrán ser unidas a develop de manera definitiva añadiéndolas así a la próxima versión.

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557)
$ git push origin develop

La orden --no-ff causa que la unión siempre cree un nuevo objeto commit. Esto evita perder información de la existencia histórica de una rama de característica y agrupa todos los commits que juntos añaden una nueva característica. En la siguiente imagen podéis verlo de una manera gráfica:

feature_branch noff

En el segundo caso es imposible comprobar qué objetos commit forman la nueva característica. Revertir dicha característica en este caso sería muy difícil, mientra que usando --no-ff es muy sencillo.

  • Ramas de lanzamiento

Se ramifican desde develop y se unen a develop y master. Su nombre de rama será release-*. Este tipo de rama ayudará a la preparación de un nuevo lanzamiento. Permiten correcciones de bugs menores y preparación del meta-data para el lanzamiento (número de versión, fechas de elaboración, etc.). Haciendo este trabajo en la rama de lanzamiento, develop estará libre para recibir las nuevas características del próximo gran lanzamiento.

El momento clave para crear una rama de lanzamiento es cuando develop muestra interés en realizar un nuevo lanzamiento próximamente. Esto es cuando las características objetivo para ese lanzamiento están ya unidas a develop. En el instante que se crea una rama de lanzamiento se le asignará un número de versión al próximo lanzamiento. Hasta ese momento develop reflejaba los cambios para el «siguiente lanzamiento». Se seguiran las reglas del proyecto para la asignación del número de versión.

La rama de lanzamiento será creada desde la rama develop. Por ejemplo, tenemos la versión 1.1.5 actualmente y tenemos un gran lanzamiento próximo. El estado de develop es listo para «el siguiente lanzamiento» y decidimos será 1.2 (en vez de 1.1.6 o 2.0):

$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

Después de crear una nueva rama y cambiar a ella, asignamos el número de versión. «bump-version.sh» es un script ficticio que cambia algunos archivos en la copia de trabajo para reflejar la nueva versión. Después esta versión es creada. La rama se mantendrá hasta que la nueva versión esté desplegada definitivamente. Durante ese tiempo, las correcciones de bugs pueden ser aplicadas en esta rama. No se podrán añadir nuevas características grandes, serán incluidas en develop o en el próximo gran lanzamiento.

Cunado el estado de la rama de lanzamiento está listo para un lanzamiento real, algunas acciones serán llevadas a cabo. Primero, la rama de lanzamiento se unirá a master (ya que todos los commit en master son por definición lanzamientos nuevos). A continuación, el commit en master debe ser etiquetado para futuras referencias de las versiones en el historial. Finalmente, los cambios realizados en la rama de lanzamiento deben ser añadidos de vuelta a develop, así los futuros lanzamientos también contendrán estas correcciones de bugs.

Los dos primeros pasos en Git:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2

El lanzamiento está ahora hecho y etiquetado para futuras referencias.

Para mantener los cambios realizados en la rama de lanzamiento, necesitamos añadirlos de vuelta en develop. En Git:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)

Este paso podría dar lugar a conflictos de unión.

Ahora la rama de lanzamiento puede ser eliminada, pues no la necesitamos más:

$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

  • Ramas de revisiónhotfix-branches

Se ramifican desde master y se unen a develop y master. Su nombre de rama será hotfix-*. Se parecen a las ramas de lanzamiento puesto que también son para nuevos lanzamientos, aunque no planeados. Surgen de la necesidad de actuar inmediatamente debido a un estado indeseado en la versión actual. Cuando un bug crítico de la versión debe resolverse inmediatamente, una rama de revisión surgirá desde la etiqueta correspondiente en la rama master que marca la versión en cuestión. La idea es que el trabajo de los miembros del equipo pueda continuar, mientras otra persona prepara una corrección rápida.

La rama de revisión será creada desde la rama master. Por ejemplo, digamos que la versión 1.2 es el lanzamiento actual y causa problemas debido a un bug grave. Pero los cambios en develop son todavía inestables. Podríamos entonces crear una rama de revisión y empezar a solucionar el problema:

$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

No os olvidéis de actualizar el número de versión después de crear la rama.

Luego corregid el bug y cread el commit o commits necesarios para la corrección.

$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

Al acabar, la corrección debe unirse de vuelta a master, pero también a develop para asegurarse de que es incluida en el siguiente lanzamiento también. Es similar a como una rama de lanzamiento es finalizada.

Primero, se actualiza master y la etiqueta de lanzamiento:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1

Lo siguiente será incluir la corrección en develop también:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

La única excepción a la regla es que, cuando existe una rama de lanzamiento, los cambios de la revisión deben ser añadidos a la rama de lanzamiento en lugar de a develop. La rama de lanzamiento acabará uniéndose a develop, por lo que la corrección también será añadida. Sin embargo, si la corrección es muy urgente también puede añadirse a develop además de a la rama de lanzamiento.

Finalmente eliminamos la rama:

$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

Aunque no hay nada nuevo realmente impactante en este modelo de ramificación, la imagen global indica que el post con el que empezamos ha resultado ser tremendamente útil en nuestros proyectos. Forma un modelo elegante que es fácil de comprender y permite a los miembros del equipo desarrollar un entendimiento compartido de la ramificación y los procesos de lanzamiento.

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