Sobre viajes en el tiempo

 

Quizá el título debería haber sido "Sobre rewinds y replays", pero este título es más bonito. Un día como cualquier otro dejo de ser tal, cuando recibimos un requerimiento del juego en el que trabajabamos por el cual debía implementarse la posibilidad de retroceder el tiempo hasta el comienzo del nivel, y que a partir de allí el jugador juegue junto al fantasma de su pasado. Esto requiere que el personaje del jugador (PJ) pueda rebobinar sus acciones, para luego volver a realizarlas. Asimismo, los enemigos deben poder rebobinarse (pero en este caso ya no es necesario un replay).

El rewind/replay PJ fue implementado con una variación del patrón de diseño command. Cada acción es un estado con un inicio, una transición y un final. Cada personaje tiene dos listas de tuplas {timestamp, action}, una para replay y otra para rewind.

La lista de replay tiene las acciones realizadas con el timestamp correspondiente al tiempo de inicio de la acción. Lo cual se hace posible volver a ejecutar dichas acciones en el mismo momento en el que fueron llamadas.

La lista de rewind tiene las mismas acciones, pero con el timestamp correspondiente al tiempo de final de dichas acciones. Esto se debe a que durante el rebobinado se debe ejecutar en reversa cada acción, pero comenzando en el momento en la que la misma finalizó.

 

El rewind/replay PJ fue implementado con una variación del patrón de diseño command. Cada acción es un estado con un inicio, una transición y un final. Cada personaje tiene dos listas de tuplas {timestamp, action}, una para replay y otra para rewind.

 

Algo importante a tener en cuenta, es que la lista de rewind se reconstruye durante cada replay, ya que una jugada posterior podría durar menos que la original, con lo cual no deberían rebobinarse las acciones que no han llegado a suceder en la jugada actual.

Otra cosa relevante, es que cada acción debe ser discreta y determinista. Es decir, deben tener un tiempo predeterminado de ejecución y su resultado debe poder ser precalculado. Esto se debe a que el tiempo de cada frame varía, lo cual causa que los tiempos en los que se ejecuta cada acción varía de una jugada a la siguiente. Si se permiten acciones continuas y no-deterministas, la ejecución de las acciones podrían comenzar a generar un desfasaje. Como ejemplo, si los movimientos son continuos, al no durar cada acción de movimiento exactamente lo mismo que en la jugada anterior, estos comenzarían a variar sus distancias levemente, acumulandose su error y llevando después de un rato al personaje a un lugar diferente al que llevó el jugador originalmente.

Los enemigos no requieren un replay, solo deben implementar un rewind. Para simplificar y hacer más eficiente su ejecución, el movimiento y accionar de los enemigos es independiente de su contexto. Es decir, no toman decisiones respecto a las acciones del jugador ni de sus compañeros. Esto permite que tanto su posición como el momento de sus acciones sean funciones del tiempo, y por lo tanto se puede, en cualquier momento, preguntar por la posición y/o acción que debería estar realizándose en cualquier otro tiempo. Esto último es lo que permite ejecutar los movimientos y acciones de los enemigos en reversa, sin necesidad de almacenar que fué pasando en cada momento.


La forma de implementar rewind/replay de los personajes del jugador implica almacenar y procesar mucha cantidad de información en todo momento, pero es inevitable, ya que el jugador es libre de hacer lo que quiera. El peso de este procesamiento fue lo que hizo poco práctico utilizar la misma estrategia para los enemigos. Puesto que su lógica de accionar es fija y determinada por nosotros, pudimos restringir su comportamiento para utilizar una estrategia mucho más eficiente en rendimiento y memoria.

 
Martin Di LisciaComment