Post-Mortem: Pocket Corps

Desde hace unas semanas he estado desarrollando un juego de “esperar” para Android. Mas que un juego, un prototipo. (Codename: “WaitingGame”). Ya sabéis, esos juegos de Android en que tomas unas cuantas acciones al día y esperas al día siguiente a que gane dinero, o se recargue una barra o que un contador arbitrario te diga que puedes jugar.

La idea inicial se trataba de un juego en el cual teníamos que manejar 3 tipos diferentes de energías (Nuclear, Coal, Eco). Las cuales se generan con el paso del tiempo y esto es representado por 3 barras en el juego, con un Maximo y “Rate” de aumento.

PocketCorps Energies

Mejoras:

El jugador, entonces, puede elegir vender energías del tipo que desee para ganar dinero (el acto de vender lo veremos mas abajo). La pega es que todas las energías tiene el mismo precio, es decir valen lo mismo. La diferencia es que al vender Energía Nuclear, el precio de la unidad de Energía se reduce, mientras que si vendemos Energía Ecológica el precio de la energía aumenta. Objetivamente, por tanto, una unidad de Energía Ecológica es superior a una de Nuclear, por ello, los precios de mejora de Energía Ecológica son mucho mas caros y es donde entran en juego las mejoras y los “materiales”.

Cada energía necesita X materiales de YYYY tipo para ser mejorada. Estos materiales tienen un precio que el jugador debe pagar, y este precio aumenta con el tiempo a medida que el jugador desarrolla mejoras.

Panel de Materiales:

PocketCorpsMaterials.JPG

Panel de Materiales (en detalle):

PocketCorpsMaterials2.JPG

Mejora de energías:

PocketCorps Energies Mejora.gif

 

Acciones:

El jugador, ademas, tiene a su disposición Acciones que puede llevar a cabo… Como por ejemplo pedir un prestamo, y bribar un poco de dinero. Estas acciones funcionan por un sistema de “Cooldowns”

PocketCorps CDs.gif

Entre ellas, la acción mas importante es la acción de vender (Sell) la cual tiene su propia interfaz y conjunto de mejoras. El jugador, mediante un sistema de “ruedas” táctiles puede incrementar o decrementar cuanto de cada uno de los tres tipos de energía quiere vender de manera granular. Ademas, la “Compañía” puede ser subida de nivel, con cada nivel el jugador puede decidir si aumentar el Máximo de energía por cada venta, o bajar el Cooldown de la acción de vender.

PocketCorps Energies Sell.gif

 

Un final

Como aficionado de los videojuegos, y en concreto como jugador ocasional de “waiting games” lo que mas me molesta de estos juegos es la falta de “final” o de “failure state”. Por ello quería traer a la mesa un “Waiting game” que tuviera un final. Para ello, diseñé todo el sistema de datos para que se basará en niveles (levels). Toda la estructura del juego gira alrededor de este concepto. Ademas, incluir el hecho de que el jugador tiene la posibilidad de “perder” y volver a empezar si no lo hace de manera optima. Así mismo, si consigue terminar el nivel de manera satisfactoria poder desbloquear el siguiente nivel.

PocketCorps Ends.JPG

Ademas del obvio sistema de “Save/Load” de Juego, orientado a dispositivos móviles. Se diseño también un sistema de “Save/Load” de Perfil de jugador, que guarda el hecho de si estamos jugando una partida ya, y que niveles tenemos bloqueados o desbloqueados.

PocketCorps Menu.JPG

 

 

La Maquina del Tiempo

Como he mencionado arriba, una característica muy común de los “Waiting Game” es la ausencia de cualquier tipo de final. Con la inclusión de un “Failure State” de repente se añade un “Reto” al jugador, cosa que cualquier otro juego de esta índole carece. Este nuevo “Reto” lleva consigo satisfacción al jugador, pero también frustración. La razón por la que en un juego como el descrito hasta ahora pueda dar frustración es el hecho de que es muy probable que el jugador olvide mirar el juego en un momento clave determinado, o que para poder ser “eficiente” tener que mirarlo constantemente. Para evitar esto incluí la “Maquina del Tiempo”. Se trata de un maquina de estados que se calcula a si misma en el futuro y que da al jugador la posibilidad de “programar” acciones en el futuro.

PocketCorps TimeMachine.gif

Ejemplo completo (Si no veis la imagen, abrid en una nueva pestaña):

Animation

 

Esto trae sus ventajas y desventajas, de las cuales hablaré mas adelante.

 

Entrando en Codigo:

La estructura de datos es aproximadamente la siguiente:

  • Profile
  • Game
    • Level
      • Materiales
      • Energias
      • Skills
      • Level Objective
      • Company

Siendo Game y todo lo que hay dentro “Serializable” para poder ser guardado y cargado por las funciones “Save/Load” (a excepción de las Lambdas, las cuales se regeneran al cargar juego, ya que no pueden ser serializadas)

Level es el conjunto de datos de se almacena virtualmente todo. Esto es así ya que al empezar nueva partida en el “Level 1” lo que estamos haciendo en copiar los datos de un “Level 1” predefinido al “Level” de nuestro “Game”. De manera que en el futuro, programar nuevos niveles, sea tan fácil como insertar nuevos números y datos.

Profile a su vez tiene su propio “Save/Load” por separado a Game y en el se almacena información básica sobre los niveles bloqueados, desbloqueados y en curso. Por ejemplo, si estamos en juego en curso, al cargar el juego, se salta el menú y carga directamente la escena de juego, tal y como hacen muchos juegos de movil actualmente.

El sistema de Tiempo

El sistema de tiempo es basado en “Ticks”: cada X segundos ocurre un tick. Cuando Abrimos el juego calcula la diferencia de tiempo entre que se cerró (ocurrió el ultimo tick) y el “ahora”, ejecuta tantos ticks como sea necesario para ponerlo al día y adelanta tantos segundos el siguiente tick como “resto” de la división (si hay un tick cada 2 minutos, y la diferencia es de 5  minutos y 10 segundos, ejecuta dos ticks y adelante el siguiente tick 1 minuto y 10 segundos).

Ademas, dentro de “Game” se almacena una lista Acciones de tiempo (TurnSequence). TurnSequence es una Acción (delegado que devuelve void) que acumula todas las acciones juntas que se deben ejecutar durante ese tick en su miembro “Sequence”  y una Lista de Acciones por separado (TurnAction) con información descriptiva de las mismas.

  • currentGame (<Game>)
    • ticks (<DictionaryTicks : Dictionary>) Cada elemento del diccionario tiene:
      • tick (<long>) (cuando se ejecuta)
      • TurnSequence (<TurnSequence>) Cada TurnSequence contiene:
        • Sequence (<Action<long, Game, bool>) (un delegado con todos los “Action” de sus TurnAction subscritos).
        • ActionList (List<TurnAction>) Cada elemento de TurnAction:
          • Action (<Action<long,Game,bool) Accion singular de este “TurnAction”
          • ActionID (<ActionID>) Identificador para encontrar esta acción en el diccionario (contiene Tick e Index de array en “ActionList”)
          • Mas Parámetros descriptivos

De esta manera lo que ocurre al programar una “Acción” en el futuro como por ejemplo una venta, es que se crea una entrada en el diccionario para el tick X, donde X es el equivalente al tiempo, y el TurnSequence apropiado anotando una Lambda (Action<long,Game,bool>), así como su respectivo “TurnAction” en “ActionList”.

Nota: Una manera rápida de describir una Lambda, es un método anónimo el cual puede ser almacenado como una variable y pasado como parámetro para ser ejecutado en otro lugar. Con ellas se suelen hacer callbacks o en videojuegos: turnos o “ticks” como es el caso. En nuestro caso las Lambdas se almacenan en “Action<long,Game,bool>” – Long indica el “tick que esta siendo ejecutado” – Game indica “sobre que instancia de Game actuar”(Cuando se calcula maquina del tiempo se actúa sobre un “Game” temporal) – y bool indica “si la lambda esta siendo invocada por un tick, o por el jugador en el presente”.

La inclusión de manera descriptiva de “TurnAction” es el “Workaround” para lidiar con el hecho de que los “Action<long,Game,bool>” (Lambdas) no pueden ser Serializados y por tanto el juego se rompe al cargar partida. TurnAction lleva la información necesaria para reconstruir la Lambda mediante la clase estática “SkillLambdas.cs”

PocketCorps skill LambdasRecibe un “TurnAction” devuelve una “Lambda”. Al cargar partida se reconstruyen las lambdas. También se reconstruyen al ser programadas (inicialmente están vacías)

El Tick

“Tick” es la función estática que lleva a cabo todo lo que hemos visto arriba.

PocketCorps Tick.JPG

Es la función que se ejecuta X veces al cargar partida para poner al día el juego o la que ocurre cuando pasan X segundos con el juego abierto. O la que, como veremos mas adelante, se ejecuta cuando se calcula los estados de la maquina del tiempo.

Notese que se le pasa (como referencia) el “Game” sobre el que tiene que actuar, en lugar de actuar directamente sobre “currentGame”.

Calculo de Maquina del tiempo

Esto se realiza en otro Hilo (Thread). Puesto que nuestro juego tienen bien definido “Codigo con API de Unity” (controllers) y Codigo de “Datos”, es decir, sin API de Unity (models) podemos usar esta opción. La condición es que es aconsejable no incluir API de Unity en un Thread que no sea el principal de nuestra aplicación.

Para esto se lanza un “job” llamado “TimeMachineJob” que hereda de “ThreadedJob” el cual, al ejecutarse en otro hilo, el juego no se queda congelado y el jugador puede seguir jugando, aunque la maquina del tiempo no esté preparada.

Durante este job, se clona de manera temporal en memoria (con el uso de ObjectCloner) una instancia del juego actual y se la hace avanzar Tick a Tick llamando a la función Tick descrita mas arriba, pasandole como Game este juego temporal. Cada X ticks (2 por defecto) se guarda una copia de este Juego Temporal en un Array. Cuando tenemos Y numero de instancias del juego en el tiempo paramos el job y enlazamos la lista con la interfaz del juego. De esta manera tenemos un Array de Estados en el futuro.

Notese el hecho de “enlazar” pues tenemos realmente 2 listas de estados de “Games” calculados en el futuro. (GameList1, GameList2) esto es de “quita y pon”, es decir, si no tenemos ninguna lista calculada, no dejamos al jugador manejar la maquina del tiempo, si tenemos al menos 1 Si le dejamos, y si está desactualizada, calculamos una nueva en la “otra” lista mientras el jugador puede seguir usando la versión desactualizada.

PocketCorps threadjob.JPG

Este es el código que se ejecuta en el Thread de generación de la maquina del tiempo.

Quiero hacer especial hincapié en esta parte.

 

Post-Mortem:

Al fin llegamos a esta parte, que esta en el titulo del post. WordPress me dice que llevo 1600+ palabras y no he hablado aun del Post-Mortem.

¿Por que está muerto? Principalmente, por que quiero. Miento. No quiero. Debo. Tras probar una build para Android, me he dado cuenta que la generación de la maquina del tiempo tarda alrededor de un 800% (aprox) mas que en las pruebas en PC en un móvil de gama media. ¿Y eso cuanto es? pues aproximadamente 50 segundos.

Esto es lo que tarda en PC:

PocketCorps TimeMachineCalcTime.gif

Esto puede parecer poco, pero hay que tener una cosa en cuenta acerca de la maquina del tiempo que no he mencionado y es que, cada vez que el jugador hace algo que pueda afectar al “futuro” la maquina del tiempo debe ser calculada de nuevo. Y tardando tantísimo es injugable en Android, ya que la maquina del tiempo es necesaria. Eso sin mencionar la cantidad de Batería y CPU que consume mientras esa barrita se llena.

¿Eso quiere decir que la idea de calcular el futuro es imposible? No, pero sin duda la manera realizada de Pocket Corps, no es la correcta. Dado que todo ha sido construido alrededor de esta idea, habria que empezar de nuevo desde cero y por tanto esta rama del juego está muerta.

¿Que solución es posible? en lugar de calcular tick a tick, calcular todos los ticks en los que “no ocurra nada” mediante simple multiplicación (si en este tick no hay acción programada por el jugador o evento por el juego, sencillamente multiplico los valores) haciendo ticks “importantes” que deben ser generados y ticks “no importantes”. Incluso calcular por Vector los ticks no importantes partiendo desde el tick importante anterior al tick importante posterior. Seguro que a otras personas se les ocurren otras ideas.

Personalmente, prefiero mirarlo desde otra perspectiva: la del diseñador, en lugar del programador:

¿Que función cumple la maquina del tiempo? La de evitar que el jugador “dependa” excesivamente del juego (como expliqué mas arriba).

¿Que otras cosas pueden sustituirla desde el punto de vista de diseño?

Pues un sistema de Cooldowns regulable. Por ejemplo: que las habilidades puedan incurrir en mas o menos Cooldown a elegir por el jugador. Cuanto mas cooldown mas potente. De esta manera el jugador puede “programar” cuando mirar el móvil de nuevo y, por ejemplo, poner cooldowns largos durante la noche y cortos durante el día (esto lo hace Clash Royale con los cofres de Plata y Oro, por ejemplo).

Ó ganar puntos de “tiempo” y el tiempo realmente no avanza hasta que el jugador gaste esos puntos en algo.

¿Que he aprendido?

No todo es en vano. He aprendido principalmente el manejo de tiempo. Estoy bastante orgulloso del manejo de tiempo (salvo la máquina del tiempo) y Lambdas. Aunque no es perfecto, el siguiente estoy convencido que será mejor. También los sistemas de datos y la independencia entre datos y controladores y su comunicación a través de llamadas y callbacks.

 

Os dejo el juego por aquí por si queréis probarlo (ojo en android chupa batería)


Descarga:

APK => .apk

Si queréis el código fuente, por favor pedídmelo por Contacto


 

 

 

Advertisements

One thought on “Post-Mortem: Pocket Corps

  1. Pingback: Delegados, Eventos y Acciones | Mecze Entertainment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s