MovieClip Factory en Flash

Para un videojuego que nos pidió un cliente tuve que solucionar un problema que me costó bastante y es por eso que lo comparto. Si bien no me gusta mucho escribir por acá sobre software privativo, en este caso encontré tan poca información que me pareció bueno compartirlo.

El juego en cuestión es un scroller horizontal, en donde van entrando chimeneas (mas bien techos con sus chimeneas) y Papá Noel tiene que hacer el delivery de regalos :). El problema apareció cuando empecé a agregar más y más techos al escenario.

La forma en que inicialmente lo manejaba era con un gran MovieClip que tenía adentro los techos en sus respectivas posiciones y luego moviendo el gran MovieClip movía todo. Simple, pero inefectivo. Según parece (aunque no hay nadie que lo confirme) Flash se lleva mal con las técnicas de clipping básicas y por eso el mover un MovieClip enorme (estamos hablando de 4000 o 5000px de ancho) le es costoso (algo así como consumir el 95% del CPU). Otra cosa que incrementaba el problema es que el juego tiene 3 layers para hacer un efecto de Parallax scrolling.

Esto hizo que empezara a investigar opciones, y la solución que mejor se adapta es crear los objetos antes de que entren en pantalla (unos pixels antes) y destruirlos al salir. Ahora bien, crear y destruir objetos on-the-fly es aún peor en muchos casos que el problema inicial :). La solución real al problema es reutilizar las instancias en lugar de eliminarlas. ¿Cómo?. Seguí leyendo.

La idea básica es que cuando un MovieClip sale de pantalla, en lugar de eliminarlo lo ocultamos. De esta forma Flash lo ignora al hacer el render. Si en algún momento necesitamos crear un objeto del mismo tipo, en lugar de crear uno nuevo, reutilizamos el que teníamos oculto y lo hacemos visible (previo acomodarlo donde corresponde). Ahora multipliquen esta idea por 10 tipos de objetos diferentes, y que además son generados al azar antes de empezar cada nivel (no puedo predecir cuantos de cada tipo necesito).

Esto hace que en lugar de tener una lista de MovieClips tengamos una lista de puntos, y solo movemos los puntos. En cada frame nos fijamos que puntos están por entrar a la pantalla y creamos el nuevo objeto y lo ponemos en la posición del punto. Mi código luce más o menos así :

// Crea los objetos que deben estar visibles
// en un momento dado.
while (i  (Stage.width+10)) {
    // Este punto está muy afuera y como
    // están ordenados, el resto también.
    break;
  }

  // Si estoy a 10 pixels de entrar lo creo
  var t:Techo = factory.create(o.techo, "techo"+last_id, 9000+last_id);
  // Acomodo el nuevo objeto, ya sea nuevo nuevo u obtenido del cache
  t._x = o.x;
  t._y = Stage.height - 125;
  // Esta lista tiene los Techos reales que estan visibles en pantalla
  techos.push(t);
  last_id++;
  ++i;
}
// Elimino los puntos que ya use
chimeneas = chimeneas.splice(i);

La solución que implementé fue esto que llamo “MovieClip Factory” que adjunto al final del post para quien quiera descargarla. La idea es que en lugar de crear un nuevo MovieClip con attachMovie, lo hago con la factory. Esta clase se encarga de manejar al lógica de caching de manera transparente.

Cuando no necesitamos más un objeto simplemente lo devolvemos a la fábrica y así esta lo deja a la espera de ser reusado. Un ejemplo de uso sería algo como sigue :

/* mc sería el MovieClip que queremos usar como padre */
factory = new Factory(mc);
factory.registerCode(1, "Techo01");

var t:MovieClip = factory.create(1, "un_id", 1);
// Devuelvo el objeto al factory.
factory.push(t);

/* A este punto t y u son el mismo objeto */
var u:MovieClip = factory.create(1, "un_id", 1);

Haciendo las cuentas, si en promedio mis techos miden 250px cada uno, de hacerlo de la manera inicial tendría que crear unos 16 techos para un escenario de 4000px de ancho. Usando el factory, suponiendo que más de 4 techos a la vez no puedo ver (por el ancho del área visible) en el peor de los casos tengo 4 de cada tipo de techo (2 completos y 2 mitades, una que entra y una que sale), es decir 20 objetos en memoria siendo el consumo mayor.

Sin embargo al generar el escenario en forma aleatoria yo valido que no toquen 2 iguales seguidos porque quedaban feos, por lo que la cuenta en realidad da 2 objetos simultáneos máximo de cada tipo, siendo 10 en total en memoria.

La otra ventaja es que puedo hacer el escenario infinitamente largo a costo constante de uso de memoria 🙂

Descargar MovieClip Factory

Lo que viene en Rails 2.2

Hace poco se anunció el RC1 de lo que será la versión 2.2 de Ruby on Rails. Si bien la fecha de salida es “cuando esté lista”, ya se pueden utilizar para aquellos que gusten vivir “on the edge”.

Para aquellos que quieran un rejunte completo de todo lo que se viene pueden ver el post : What’s New in Edge Rails: Rails 2.2 Released – Summary of Feature.

Yo acá voy a comentar solo lo que tuve la oportunidad de probar y que me resultó útil.

I18n

La verdad es que es una buena noticia, no para mi porque hago todo para un solo idioma :P, pero algún día puede resultar útil. Lo único que a mi me deja con sabor a poco es la forma en que se hace que no me termina de gustar. Antes que nada quiero aclarar, porque ya vi la pregunta en un par de listas de correo, que esto localiza textos, no el contenido de una instancia de ActiveRecord. Si uno quiere que el contenido que cargan los usuarios sea traducido, es otra historia :).

Las traducciones se escriben en ruby o en archivos YAML, con “hashes” que pueden estar anidados (como si fueran “namespaces“) :

# lib/locale/en-US.rb
{ 'en-US' => {
  :hello_world => "Hello World",
  :hello_flash => "Hello Flash"
}}
 
# lib/locale/pirate.rb
{ 'pirate' => {
  :hello_world => "Ahoy World",
  :hello_flash => "Ahoy Flash"
}}
 

Y luego en las vistas (o mailers o donde sea) en lugar de poner el texto se ingresa el symbol asociado al texto que cargamos en las traducciones, por ejemplo :

Mi problema particular con este método es que estoy mucho más acostumbrado a cómo se hace con gettext (que no necesariamente es la mejor forma) donde se ponen todos los textos en inglés en la aplicación y después se escriben las traducciones.

Hay un demo online acá, que en estos momentos está caído, espero que para cuando lean esto ya este funcionando de nuevo.

Join Tables Conditions

Esto es algo que realmente hacía falta. Vamos a ver la mejora con un ejemplo. Supongamos que tenemos los siguientes modelos :

class Article < ActiveRecord::Base
  belongs_to :user
end

class User < ActiveRecord::Base
  has_many :articles
end

y queremos obtener todos los usuarios que tengan al menos un artículo publicado. Para eso deberíamos hacer :

User.find(:all, :joins => :article,  :conditions => ["articles.published = ?", true])

Con la nueva sintaxis es posible especificar este tipo de queries de una manera más amena como sigue :

User.find(:all, :joins => :article, :conditions => { :articles => { :published => true } })

ActionMailer Layouts

Algo que si me tocó vivir es tener varios emails HTML con un mismo formato y que cambiaba el contenido (Tu amigo te invitó, Tu nueva clave es, Ganaste un premio, etc). Con el mailer actual casi que hay que hacer un copy & paste del marco y despuer cambiar para cada tipo de email la lógica de qué se muestra.

En rails 2.2 ahora vamos a tener layouts como los tenemos en ActionView, de manera de tenerlo una vez y si arreglamos o cambiamos algo cambia para todos.

Memoization

Es muy frecuente que uno agregue lógica simple de caching en los modelos para las variables de instancia como se ve en el siguiente ejemplo :

  class User < ActiveRecord::Base
    def full_name
      @full_name ||= "#{first_name} #{last_name}"
    end
  end

Esto lo hacemos para evitar el overhead de crear el full_name si lo llegamos a usar varias veces en una misma vista, de manera que el string se crea en la primer llamada y en las siguientes solamente lo retorna.

Los puristas dicen que en realidad está mal, porque se está responsabilizando al método de algo que en realidad no debería importarle : la política de caching. ¿Suena exagerado? Seguramente :).

Para solucionar esto se agregó el método memoize que nos permite a agregar a un método común este tipo de lógica de manera separada :

  class User < ActiveRecord::Base
    def full_name
      "#{first_name} #{last_name}"
    end

    memoize :full_name
  end

Esto se encarga de que cuando llamemos a @user.full_name se comporte de la misma manera que el primer ejemplo, sin tener que modificar el método. También nos permite saltarnos el “cache”, por ejemplo si en la lógica acabamos de cambiar el nombre de pila, debemos forzar para que el nombre completo cambie :

   @user = User.new :first_name => 'Test', :last_name => 'Test'
   @user.full_name # => Test Test
   @user.first_name = 'Do'
   @user.full_name # => Test Test
   @user.full_name(true) # => Do Test

Y más

Hay varios cambios más, como Thread Safety, ETag, partial GETs y demás que a mi por ahora no me interesan y por eso no me entiendo mucho más. Pueden consultar el anuncio oficial para ver de que se tratan o cómo pueden hacerle la vida más fácil :).

Usando Rails.cache en 2.1.x

Para uno de los sitios que tenemos montados llegó la hora de optimizar algunas partes para descargar la DB un poco. Lo primero (y por ahora único :P) que se hizo fue cachear la instancia que representa a la Etapa actual del juego en memoria, para así no cargarla en cada request.

El código original era :

class Stage  ["start_at = ?", t, t])
  end
end

y lo cambiamos para usar el nuevo sistema de cache de Rails 2.1.x que realmente fue simplificado. Hay varios sitio donde hablan sobre el cache en 2.1, les recomiendo mirar el screencast de RailsCasts y este y este post.

El código modificado quedó como se muestra a continuación.

# config.environment.rb
config.cache_store = :mem_cache_store

# app/models/stage.rb
class Stage  ["start_at = ?", t, t])
      Rails.cache.write('stage_current', @current)
    end
    @current
  end

La condición del if es necesaria por dos motivos : la primera por si el cache fue limpiado y la segunda para invalidar el valor guardado actualmente si la etapa eterminó, para que la nueva etapa pase a ocupar el cache.

En este caso estamos usando MemCache como CacheStore ya que se comparte entre más de un webserver y simplifica expirar el cache.

Lo único que resta es expirar el cache en caso de que cambie el modelo, que además de la fecha de finalización contiene textos que son usados en la web (como las bases y condiciones, premios, etc). Para eso creamos un Observer que se encargue de eso cuando una etapa es guardada y además es la etapa actual (si no, no tiene sentido hacer nada) :

# app/models/stage_observer.rb
class StageObserver < ActiveRecord::Observer
  def after_save(stage)
    Rails.cache.delete('stage_current') if stage.is_current?
  end
end

# config/environment.rb
config.active_record.observers = :stage_observer

Con esto bajó bastante el uso de la DB (prácticamente no se puede hacer nada en el sitio sin consultar la etapa actual). Ahora tengo que ver que le pasa a la DB que a veces respondes después de 3 segs :S, pero eso ya escapa a programar :).