viernes, 5 de marzo de 2010

Probando Unity3D

Estuve probando Unity3D como una forma de hacer juegos web. En el espíritu de "dejar todo asentado", muestro y explico los resultados.

Después de implementar el tutorial de plataformas, me largué a hacer un pequeño asset.

El desafío:
  • Hacerlo con scripts en C# (lenguaje que desconozco)
  • Interactuar con algún servicio web, usando JSON como tecnología
El resultado:

Lector de twitter en Unity3D

La primera impresión es que hacer scripts es extremadamente intuitivo. Si a alguien le interesa le puedo exportar el asset completo para que lo examinen en detalle.

Para comunicarme con Twitter, usé JSON. Existen muchos parsers JSON para C#, pero el que yo usé es "LitJson". Usarlo es tan simple como hacer un drag and drop de todos los archivos .cs de la distribución al árbol del proyecto Unity.

Paso a explicar el script:

using UnityEngine;
using System.Collections;
using LitJson;

public class TwitterPlatform : MonoBehaviour {
 public Camera camera;
 public Transform player;
 public string queryString="pozzoeRand";
 private Transform messageBoard;
 private ParticleEmitter emitter;
 private string twitterText;
 private MeshRenderer meshRender;
 private bool active=false;

No hay mucho de decir. A diferencia de Unity+Javascript, todo nuestro código debe ser implementado en una clase hija de MonoBehaviour. El nombre de la clase debe ser igual al archivo donde se encuentra el script.

Gracias a la magia de Unity, todas las variables públicas de la clase, son puestas en el object Inspector de Unity y pueden ser configuradas como parámetros del script. Instalar un script en un objeto implica haber instanciado un objeto de la clase del script. Y cada objeto tiene su propia variable. Eso hace que uno pueda tener un "query string" distinto en cada lector Twitter.

La variables "camera" y "player" deben ser configuradas al momento de instanciar un objeto que hace uso de este script. La forma de configurarlas es simplemente arrastrar una cámara y un jugador al slot correspondiente en el inspector de objetos. La cámara y el jugador se usan para permitirle al script acomodar el mensaje de texto de modo tal que sea siempre visible por el jugador.

El flag active indica si la plataforma está activada. El string twitterText es el texto a mostrar en el cartel de texto.

void Start () {
  this.emitter=this.transform.Find("CommunicationEffect")
    .GetComponent();
  this.messageBoard=this.transform.Find("TextRenderer");
  this.meshRender=this.messageBoard.GetComponent();
  this.meshRender.enabled=true;
  this.setDefaultText();
 }

La función Start es llamada automáticamente cuando se inicia la escena (el juego en este caso). En este script ajusto algunas variables privadas. Los detalles son mas evidentes al ver la estructura del objeto en si. El objeto donde está ubicado el script, tiene objetos hijos que se pueden acceder con la función Find. El objeto CommunicationEffect es un "efecto especial" que utiliza partículas para mostrar al usuario que está "pasando algo".

Por otro lado, el objeto "TextRenderer" es un cartel de texto que va a ser usado para mostrar el resultado de los queries al Api Twitter y también para mostrar el querystring antes que el jugador haya activado la plataforma.

void Update () {
  this.meshRender.transform.forward = this.camera.transform.forward;
  if (this.active==false)
   return;

  this.meshRender.transform.position = 
  this.player.transform.position 
  + this.camera.transform.forward*20
  + this.camera.transform.up*8;
 }

La función update es llamada automáticamente en cada frame del juego. Lo primero que hace es asegurarse que el cartel está paralelo al plano de la cámara, para que sea visto claramente.

Después, si la plataforma está desactivada (el jugador no ha pisado la plataforma), termina.

Si la plataforma está activada, la función update se asegura de que el cartel de texto esté puesto en un lugar de la plataforma que permita al jugador leer el texto. Las variables forward y up son vectores en las coordenadas locales del objeto (en este caso la cámara).

void OnTriggerEnter(){
  this.emitter.Emit();
  StartCoroutine(this.readTwitter());
  this.messageBoard.GetComponent().transform.position=this.player.transform.position+this.camera.transform.forward*20+this.camera.transform.up*8;
  this.active=true;
 }

La función OnTriggerEnter es llamada cuando el engine detecta una colisión con el mesh del objeto en el que se encuentra el script. Mas adelante muestro dos funciones relacionadas.

La función Emit() es llamada sobre el emisor de partículas. Hace que se emita un chorro de partículas. Luego, se inicia una corrutina que es la que se encarga de interactuar con twitter. Una corrutina es una función que puede interrumpir su ejecución para luego resumir en el mismo punto. La función StartCoroutine, usada de esta forma, ejecuta la corrutina readTwitter en paralelo a la función update, y continua ejecutando la funcion update.

IEnumerator readTwitter()
 {
  while (true) {
   string url="http://search.twitter.com/search.json?q="+this.queryString;
   this.twitterText="Loading";
   WWW tweets = new WWW (url); 
   
   int retries=0;
   bool  reload=false;
   while (!tweets.isDone){
    if (retries>20) {
     /*Will retry after trying 0.2*20 seconds*/
     this.twitterText="Loading";
     retries=0;
     reload=true;
     break;
    }
    this.twitterText+=".";
    yield return new WaitForSeconds(0.2f);
    retries++;
   }
   if (reload)
    continue;

Esta función es la que lee de Twitter. Recordemos que se está ejecutando en paralelo con el resto de las funciones. Y en particular, esta corrutina se está ejecutando en un loop infinito.
Lo que se hace es crear una petición al servidor de Twitter. Y después esperar hasta que Twitter responda. El problema es que no sabemos cuanto se puede demorar Twitter (con sus magnificos servers) en responder. Y aca es cuando entra la magia de las corrutinas. Cada "yield" es como un return, que devuelve la ejecución para que el engine sigua funcionando, con la particularidad de que al llamar la corrutina en el próximo frame la ejecución se continua en el yield, como si nunca se hubiera abandonado.
En particular se puede hacer otras cosas como yield return new WaitForSeconds(0.2f), hace que unity suspenda la ejecución de la corrutina y espere 0.2 segundos antes de continuar. De esta manera, en cada paso se le pregunta al objeto "tweet" si ya terminó de cargar todo lo que twitter le devolvió. Si no, se aumenta un contador y se espera 0.2 segundos antes de volver a preguntar. Si el server tarda mas de 4 segundos en responder, se vuelve a empezar desde el principio, o sea, se envía otro query (al menos es lo que teóricamente debería pasar, no lo probé, si alguien piensa lo contrario que hable).

En caso contrario, la corrutina continua su ejecución.

JsonData queryResult=JsonMapper.ToObject(tweets.data);
   JsonData tweetList=queryResult["results"];
   for (int i=0;i<tweetList.Count; i++)
   {
    this.messageReceived();
    this.twitterText=wrap((string)tweetList[i]["text"]);
    yield return new WaitForSeconds(4f);

   }

  } 
 }
Esta parte de la corrutina usa el parser de JSON para leer uno a uno los tweets. No hay mucho que sea especifico de Unity. Solo que nuevamente se espera 4 segundos entre tweet y tweet, para darle tiempo al jugador de que lea cada uno.

Este lector de twitter es absolutamente precario e ineficiente. Lo ideal sería que se muestren los tweets de mas viejo a mas nuevo y luego se actualizen los nuevos tweets a medida que la gente los va publicando. Todo eso es posible, pero no tiene sentido perder mas tiempo en este script que es solo de prueba.

La función MessageReceived simplemente activa el efecto del emisor de partículas. Se puede extender para que también reproduzca un sonido, por ejemplo.

void OnTriggerStay() {
  this.messageBoard.GetComponent().text=this.twitterText;
  
 }
 void OnTriggerExit(){
  StopAllCoroutines();
  this.active=false;
  this.setDefaultText();
 }
La función OnTriggerStay es ejecutada una y otra vez mientras el jugador está en contacto con la plataforma. Lo único que hace es mantener actualizado el texto de la plataforma. Tranquilamente podría haberse hecho en la función Update, luego de comprobar que la plataforma se encuentra activa.

La función OnTriggerExit es ejecutada automáticamente cuando se abandona la plataforma. En este caso, se detiene la ejecución de todas las corrutinas de este objeto y luego se vuelve a mostrar el texto con el querystring ubicado en el centro de la plataforma.

void setDefaultText()
 {
  this.meshRender.transform.position=this.transform.position+this.meshRender.transform.up*1;
  this.messageBoard.GetComponent().text=this.queryString;
 }
 void messageReceived(){
  this.emitter.Emit();
 }
 string wrap(string text)
 {
  string newText="";
  text=text.PadRight(140);
  for (int i=0; i<text.Length-28; i+=28)
  {
   newText+= text.Substring(i, 28)+"\n";
  }
  return newText;
 }

Estas son funciones de ayuda. La única que no mencioné es wrap, que simplemente distribuye el mensaje en varias lineas.

Este script es incompleto. Sería adecuado considerar errores como que twitter no devuelva tweets, y probar algunos detalles del funcionamiento.

Pero esto no es nada mas que un proyecto descartable para probar algunas de las características de Unity.

La idea inicial era implementar el automata celular mostrado anteriormente en Unity. Pero no estoy seguro de que sea lo mejor la performance de los scripts para ese tipo usos. Lo ideal sería extender la funcionalidad de Unity usando C++, pero esa es una característica disponible solamente en la version Pro (i.e. paga).

2 comentarios:

Podés comentar lo que quieras. Si puteas, perdés.

Licencia


Creative Commons License
Esta obra de Ezequiel Pozzo se encuentra bajo una licencia Creative Commons Atribución-No Comercial-Compartir Obras Derivadas Igual 2.5 Argentina License.
Se puede obtener permisos mas allá de los otorgados por esta licencia en ezequielpozzo.blogspot.com.