Es ingenuo. Super: código y arquitectura de un juego simple

Vivimos en un mundo complejo y, al parecer, hemos comenzado a olvidarnos de cosas simples. Por ejemplo, sobre la navaja de afeitar de Occam, cuyo principio es: "Lo que se puede hacer sobre la base de un número menor no se debe hacer sobre la base de uno mayor". En este artículo hablaré sobre soluciones simples y no las más confiables que se pueden usar en el desarrollo de juegos simples.



Para el otoño DotNext en Moscú, decidimos desarrollar un juego. Fue una variación de TI de la popular Alquimia. Los jugadores tuvieron que recopilar 128 conceptos relacionados con TI, pizza y Dodo de los 4 elementos disponibles. Y necesitábamos darnos cuenta de esto desde una idea hasta un juego de trabajo en poco más de un mes.

En un artículo anterior , escribí sobre el componente de diseño del trabajo: planificación, fakapy y emociones. Y este artículo trata sobre la parte técnica. ¡Incluso habrá algún código!

Descargo de responsabilidad: los enfoques, el código y la arquitectura que escribo a continuación no son complicados, originales o confiables. Por el contrario, son muy simples, a veces ingenuos y no están diseñados para cargas pesadas por diseño. Sin embargo, si nunca ha creado un juego o una aplicación que utilice la lógica en el servidor, este artículo puede servir como impulso inicial.

Codigo del cliente


En pocas palabras sobre la arquitectura del proyecto: teníamos clientes móviles para Android e iOS en Unity y un servidor back-end en ASP.NET con CosmosDB como almacenamiento.

Un cliente en Unity representa solo la interacción con la interfaz de usuario. El jugador hace clic en los elementos, se mueven por la pantalla de forma fija. Cuando se crea un nuevo elemento, aparece una ventana con su descripción.


El

proceso del juego Este proceso puede ser descrito por una máquina de estados bastante simple. Lo principal en esta máquina de estado es esperar la animación de la transición al siguiente estado, bloqueando la interfaz de usuario del jugador.



Usé la genial biblioteca UnitRx para escribir código Unity en un estilo completamente asíncrono. Al principio intenté usar mi Tarea nativa, pero se comportaron inestables en las compilaciones para iOS. Pero UniRx.Async funcionó como un reloj.

Cualquier acción que requiera animación se llama a través de la clase AnimationRunner:

public static class AnimationRunner
   {
       private const int MinimumIntervalMs = 20;
     public static async UniTask Run(Action<float> action, float durationInSeconds)
       {
           float t = 0;
           var delta = MinimumIntervalMs / durationInSeconds / 1000;
           while (t <= 1)
           {
               action(t);
               t += delta;
               await UniTask.Delay(TimeSpan.FromMilliseconds(MinimumIntervalMs));
           }
       }
   }

En realidad, esto está reemplazando la clásica rutina con UnitTask. Además, cualquier llamada que deba bloquear la IU se invoca a través de un método de HandleUiOperationclase global GameManager:

public async UniTask HandleUiOperation(UniTask uiOperation)
       {
           _inputLocked = true;
           await uiOperation;
           _inputLocked = false;
       }

En consecuencia, en todos los controles, el valor de InputLocked se verifica primero, y solo si es falso, el control reacciona.

Esto facilitó la implementación de la máquina de estado que se muestra arriba, incluidas las llamadas de red y E / S, utilizando el enfoque asíncrono / espera con llamadas anidadas, como en una muñeca rusa.

La segunda característica importante del cliente fue que todos los proveedores que recibieron datos sobre los elementos se hicieron en forma de interfaces. Después de la conferencia, cuando apagamos nuestro back-end, permitió literalmente en una noche reescribir el código del cliente para que el juego quedara completamente fuera de línea. Esta versión se puede descargar ahora desde Google Play .

Interacción cliente-respaldo


Ahora hablemos sobre las decisiones que tomamos al desarrollar la arquitectura cliente-servidor.



Las imágenes se almacenaron en el cliente, y el servidor fue responsable de toda la lógica. Al inicio, el servidor leyó el archivo csv con identificación y descripciones de todos los elementos y los almacenó en su memoria. Después de eso, estaba listo para partir.

Los métodos API eran un mínimo necesario, solo cinco. Implementaron toda la lógica del juego. Todo es bastante simple, pero te contaré algunos puntos interesantes.

Autenticación y elementos iniciales


Abandonamos cualquier sistema de autenticación complicado y, en general, las contraseñas. Cuando un jugador ingresa un nombre en la pantalla de inicio y presiona el botón "Inicio", se crea una ficha (ID) aleatoria única en el cliente. Sin embargo, no está conectado al dispositivo. El nombre del jugador junto con el token se envían al servidor. Todas las demás solicitudes del cliente a la parte posterior contienen este token.



Las desventajas obvias de esta solución son:

  1. Si el usuario demuele la aplicación y la reinstala, se lo considerará un jugador nuevo y se perderá todo su progreso.
  2. No puedes continuar el juego en otro dispositivo.

Estas fueron suposiciones deliberadas, porque entendimos que las personas jugarían en la confe desde un solo dispositivo. Y no tendrán tiempo de cambiar a otro dispositivo.

Por lo tanto, en el escenario planificado, el cliente llamó al método del servidor AddNewUsersolo una vez.

Al cargar la pantalla del juego GetBaseElements, también se llamó una vez al método , que devolvió id, el nombre del sprite y la descripción de los cuatro elementos básicos. El cliente encontró los sprites necesarios en sus recursos, creó los objetos de los elementos, los escribió localmente y los pintó en la pantalla.

Tras repetidos lanzamientos, el cliente ya no se registró en el servidor y no solicitó elementos de inicio, sino que los retiró del almacenamiento local. Como resultado, la pantalla del juego se abrió inmediatamente.

Fusionar elementos


Cuando un jugador intenta conectar dos elementos, se llama a un método MergeElementsque devuelve información sobre el nuevo elemento o informa que estos dos elementos no se recopilan. Si el jugador ha recopilado un nuevo elemento, la información sobre esto se registra en la base de datos.



Aplicamos la solución obvia: para reducir la carga en el servidor, después de que el jugador intenta agregar dos elementos, el resultado se almacena en caché en el cliente (en memoria y en csv). Si un jugador intenta volver a apilar elementos, el caché se verifica primero. Y solo si el resultado no está allí, la solicitud se envía al servidor.

Por lo tanto, sabemos que cada jugador puede realizar un número limitado de interacciones con el reverso, y el número de entradas en la base de datos no excede el número de elementos disponibles (de los cuales teníamos 128). Pudimos evitar los problemas de muchas otras aplicaciones de conferencia, que luego de una gran afluencia simultánea de participantes a menudo respaldaron.

Tabla de puntaje alto


Los participantes jugaron nuestra "Alquimia" no solo así, sino por el bien de los premios. Por lo tanto, necesitábamos una tabla de registros, que mostramos en la pantalla en nuestro stand, y también en una ventana separada en nuestro juego.



Para formar una tabla de registros, se utilizan los dos últimos métodos GetCurrentLaddery GetUser.también hay un matiz curioso. Si un jugador está entre los 20 mejores resultados, su nombre se resalta en la tabla. El problema era que la información de estos dos métodos no estaba directamente relacionada.

El método GetCurrentLadderaccede a la colección Stats, obtiene 20 resultados y lo hace rápidamente. El método GetUseraccede a la colección.Userspor UserId y lo hace demasiado rápido. La fusión de los resultados ya está en el lado del cliente. Eso es solo que no queríamos destacar UserId en los resultados, por lo que no están allí. La comparación se realizó según el nombre del jugador y el número de puntos anotados. En el caso de miles de jugadores, las colisiones serían inevitablemente. Pero contamos con el hecho de que entre todos los jugadores es poco probable que haya jugadores con los mismos nombres y puntos. En nuestro caso, este enfoque está totalmente justificado.

Juego terminado


Nuestra tarea inicial era armar un juego para una conferencia de dos días en un mes. Todas esas decisiones que encarnamos en arquitectura y código se han justificado por completo. El servidor no se recostó, todas las solicitudes se procesaron correctamente y los jugadores no se quejaron de errores en la aplicación.

Para el próximo DotNext de Moscú, es muy probable que agitemos otro juego, porque ahora se ha convertido en nuestra buena tradición ( CMAN-2018 , IT-Alchemy-2019 ). Escriba en los comentarios para qué asesino de tiempo está listo para intercambiar informes hardcore de estrellas de desarrollo. :)
Para los mismos ingenuos e interesados, hemos publicado el código del cliente de alquimia de TI en el dominio público .

Y mira el canal de telegramas , donde escribo todo sobre desarrollo, vida, matemáticas y filosofía.

All Articles