C'est naïf. Super: code et architecture d'un jeu simple

Nous vivons dans un monde complexe et, semble-t-il, nous avons commencé à oublier les choses simples. Par exemple, à propos du rasoir d'Occam, dont le principe est: "Ce qui peut être fait sur la base d'un plus petit nombre ne devrait pas être fait sur la base d'un plus grand." Dans cet article, je parlerai de solutions simples et non des plus fiables pouvant être utilisées dans le développement de jeux simples.



À l'automne DotNext à Moscou, nous avons décidé de développer un jeu. C'était une variation informatique de l'alchimie populaire. Les joueurs devaient collecter 128 concepts liés à l'informatique, à la pizza et au Dodo parmi les 4 éléments disponibles. Et nous devions réaliser cela d'une idée à un jeu de travail en un peu plus d'un mois.

Dans un article précédent , j'ai écrit sur la composante design du travail: planification, fakapy et émotions. Et cet article concerne la partie technique. Il y aura même du code!

Avertissement: Les approches, le code et l'architecture que j'écris ci-dessous ne sont pas compliqués, originaux ou fiables. Au contraire, ils sont très simples, parfois naïfs et non conçus pour des charges lourdes par conception. Cependant, si vous n'avez jamais créé de jeu ou d'application utilisant la logique sur le serveur, cet article peut servir d'élan de démarrage.

Code client


En bref sur l'architecture du projet: nous avions des clients mobiles pour Android et iOS sur Unity et un serveur backend sur ASP.NET avec CosmosDB comme stockage.

Un client sur Unity représente uniquement une interaction avec l'interface utilisateur. Le joueur clique sur les éléments, ils se déplacent sur l'écran de manière fixe. Lorsqu'un nouvel élément est créé, une fenêtre apparaît avec sa description.


Le

processus de jeu Ce processus peut être décrit par une machine d'état assez simple. L'essentiel dans cette machine d'état est d'attendre l'animation de la transition vers l'état suivant, bloquant l'interface utilisateur pour le joueur.



J'ai utilisé la bibliothèque UnitRx très cool pour écrire du code Unity dans un style complètement asynchrone. Au début, j'ai essayé d'utiliser ma tâche native, mais ils se sont comportés de manière instable sur les versions pour iOS. Mais UniRx.Async fonctionnait comme une horloge.

Toute action nécessitant une animation est appelée via la classe 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));
           }
       }
   }

Il s'agit en fait de remplacer la coroutine classique par UnitTask. De plus, tout appel qui devrait bloquer l'interface utilisateur est appelé via une méthode de HandleUiOperationclasse globale GameManager:

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

Par conséquent, dans tous les contrôles, la valeur de InputLocked est d'abord vérifiée et seulement si elle est fausse, le contrôle réagit.

Cela a rendu assez facile l'implémentation de la machine d'état décrite ci-dessus, y compris les appels réseau et les E / S, en utilisant l'approche asynchrone / attente avec les appels imbriqués, comme dans une poupée russe.

La deuxième caractéristique importante du client est que tous les fournisseurs qui ont reçu des données sur les éléments ont été créés sous forme d'interfaces. Après la conférence, lorsque nous avons éteint notre backend, cela a permis littéralement en une soirée de réécrire le code client afin que le jeu soit complètement hors ligne. Cette version peut être téléchargée depuis Google Play .

Interaction client-arrière


Parlons maintenant des décisions que nous avons prises lors du développement de l'architecture client-serveur.



Les images étaient stockées sur le client et le serveur était responsable de toute la logique. Au démarrage, le serveur a lu le fichier csv avec l'id et les descriptions de tous les éléments et les a stockés dans sa mémoire. Après cela, il était prêt à partir.

Les méthodes API étaient un minimum nécessaire - seulement cinq. Ils ont implémenté toute la logique du jeu. Tout est assez simple, mais je vais vous parler de quelques points intéressants.

Authentification et éléments de démarrage


Nous avons abandonné tout système d'authentification compliqué et généralement tous les mots de passe. Lorsqu'un joueur entre un nom sur l'écran de démarrage et appuie sur le bouton "Démarrer", un jeton aléatoire unique (ID) est créé dans le client. Cependant, il n'est pas attaché à l'appareil. Le nom du joueur ainsi que le jeton sont envoyés au serveur. Toutes les autres demandes du client vers l'arrière contiennent ce jeton.



Les inconvénients évidents de cette solution sont:

  1. Si l'utilisateur démolit l'application et la réinstalle, il sera considéré comme un nouveau joueur et toute sa progression sera perdue.
  2. Vous ne pouvez pas continuer le jeu sur un autre appareil.

Ce sont des hypothèses délibérées, car nous avons compris que les gens joueraient sur le confe uniquement à partir d'un seul appareil. Et ils n'auront pas le temps de passer à un autre appareil.

Ainsi, dans le scénario prévu, le client AddNewUsern'a appelé la méthode serveur qu'une seule fois.

Lors du chargement de l'écran de jeu GetBaseElements, la méthode était également appelée une fois , ce qui renvoyait id, le nom du sprite et la description des quatre éléments de base. Le client a trouvé les sprites nécessaires dans ses ressources, créé les objets des éléments, les a écrit localement et peint à l'écran.

Lors de lancements répétés, le client ne s'est plus enregistré sur le serveur et n'a pas demandé d'éléments de démarrage, mais les a extraits du stockage local. En conséquence, l'écran de jeu s'est immédiatement ouvert.

Fusionner des éléments


Lorsqu'un joueur essaie de connecter deux éléments, une méthode est appelée MergeElementsqui retourne des informations sur le nouvel élément ou signale que ces deux éléments ne sont pas collectés. Si le joueur a collecté un nouvel élément, des informations à ce sujet sont enregistrées dans la base de données.



Nous avons appliqué la solution évidente: pour réduire la charge sur le serveur, après que le joueur a essayé d'ajouter deux éléments, le résultat est mis en cache sur le client (en mémoire et en csv). Si un joueur essaie de réempiler des éléments, le cache est d'abord vérifié. Et seulement si le résultat n'est pas là, la demande est envoyée au serveur.

Ainsi, nous savons que chaque joueur peut effectuer un nombre limité d'interactions avec le dos, et le nombre d'entrées dans la base de données ne dépasse pas le nombre d'éléments disponibles (dont nous avions 128). Nous avons pu éviter les problèmes de nombreuses autres applications de conférence qui, après un afflux important et simultané de participants, ont souvent reculé.

Tableau des meilleurs scores


Les participants ont joué notre "Alchimie" non seulement comme ça, mais pour des prix. Par conséquent, nous avions besoin d'un tableau des enregistrements, que nous avons affiché à l'écran sur notre stand, et également dans une fenêtre séparée dans notre jeu.



Pour former un tableau des enregistrements, les deux dernières méthodes sont utilisées GetCurrentLadderet GetUser.il y a aussi une nuance curieuse. Si un joueur figure parmi les 20 premiers résultats, son nom est mis en évidence dans le tableau. Le problème était que les informations de ces deux méthodes n'étaient pas directement liées.

La méthode GetCurrentLadderaccède à la collection Stats, obtient 20 résultats et le fait rapidement. La méthode GetUseraccède à la collection.Userspar UserId et le fait trop rapidement. La fusion des résultats se fait déjà côté client. C'est juste que nous ne voulions pas briller UserId dans les résultats, ils ne sont donc pas là. La comparaison a eu lieu en fonction du nom du joueur et du nombre de points marqués. Dans le cas de milliers de joueurs, les collisions seraient inévitablement. Mais nous avons compté sur le fait que parmi tous les joueurs, il est peu probable qu'il y ait des joueurs avec les mêmes noms et points. Dans notre cas, cette approche est pleinement justifiée.

Jeu terminé


Notre tâche initiale était de monter un jeu pour une conférence de deux jours dans un mois. Toutes ces décisions que nous avons incarnées dans l'architecture et le code se sont pleinement justifiées. Le serveur ne s'est pas couché, toutes les demandes ont été traitées correctement et les joueurs ne se sont pas plaints de bugs dans l'application.

D'ici le prochain Moscou DotNext, il est fort probable que nous remuions un autre jeu, car il est devenu notre bonne tradition ( CMAN-2018 , IT-Alchemy-2019 ). Écrivez dans les commentaires pour quel tueur de temps vous êtes prêt à échanger des rapports hardcore de stars du développement. :)
Pour les mêmes naïfs et intéressés, nous avons mis le code client de l'alchimie informatique dans le domaine public .

Et regardez le canal des télégrammes , où j'écris tout sur le développement, la vie, les mathématiques et la philosophie.

All Articles