
Dans la première partie de l'article, nous avons examiné comment le modèle doit être organisé de manière à être facile à utiliser, mais le débogage et le vissage des interfaces sont simples. Dans cette partie, nous considérerons le retour des commandes de changements dans le modèle, dans toute sa beauté et sa diversité. Comme précédemment, la priorité pour nous sera la commodité du débogage, en minimisant les gestes qu'un programmeur doit faire pour créer une nouvelle fonctionnalité, ainsi que la lisibilité du code pour une personne.
Solutions architecturales pour un jeu mobile. Partie 1: ModèleSolutions architecturales pour un jeu mobile. Partie 3: Vue sur la poussée du jetPourquoi commander
Le modèle de commande sonne fort, mais en fait c'est juste un objet dans lequel tout ce qui est nécessaire pour l'opération demandée y est ajouté et stocké. Nous choisissons cette approche, au moins parce que nos équipes seront envoyées sur le réseau, et même nous obtiendrons quelques copies de l'état du jeu pour un usage officiel. Ainsi, lorsque l'utilisateur clique sur le bouton, une instance de la classe de commandes est créée et envoyée au destinataire. La signification de la lettre C dans l'abréviation MVC est quelque peu différente.
Prédiction des résultats et vérification des commandes sur le réseau
Dans ce cas, le code spécifique est moins important que l'idée. Et voici l'idée:
Un jeu qui se respecte ne peut pas attendre une réponse du serveur avant de réagir au bouton. Bien sûr, Internet s'améliore et vous pouvez avoir un tas de serveurs partout dans le monde, et je connais même quelques jeux réussis en attente d'une réponse du serveur, l'un d'eux est même Summoning Wars, mais vous n'avez toujours pas besoin de le faire. Parce que pour l'internet mobile, des retards de 5 à 15 secondes sont plus susceptibles d'être la norme qu'une exception, à Moscou au moins, le jeu devrait être vraiment génial pour que les joueurs n'y fassent pas attention.
En conséquence, nous avons un état de jeu qui représente toutes les informations nécessaires à l'interface, et les commandes lui sont appliquées immédiatement, et seulement après cela, elles sont envoyées au serveur. Habituellement, les programmeurs java assidus sont assis sur le serveur, dupliquant toutes les nouvelles fonctionnalités une à une dans une autre langue. Sur notre projet «cerfs», leur nombre a atteint 3 personnes, et les erreurs commises lors du portage ont été une source constante de joie insaisissable. Au lieu de cela, nous pouvons le faire différemment. Nous exécutons sur le serveur .Net et exécutons côté serveur le même code de commande que sur le client.
Le modèle décrit dans le dernier article nous offre une nouvelle opportunité intéressante pour l'auto-test. Après avoir exécuté la commande sur le client, nous calculerons le hachage de la modification survenue dans l'arborescence GameState et l'appliquerons à l'équipe. Si le serveur exécute le même code de commande et que le hachage des modifications ne correspond pas, alors quelque chose s'est mal passé.
Premiers avantages:
- Cette solution accélère considérablement le développement et minimise le nombre de programmeurs de serveurs.
- Si le programmeur a fait des erreurs conduisant à un comportement non déterministe, par exemple, il a obtenu la première valeur du dictionnaire, ou a utilisé DateTime.now, et a généralement utilisé certaines valeurs non écrites explicitement dans les champs de commande, puis quand heh démarre sur le serveur, elles ne correspondront pas, et nous le découvrirons.
- Le développement client peut pour l'instant se faire sans serveur du tout. Vous pouvez même passer en alpha convivial sans avoir de serveur. C'est utile non seulement pour les développeurs indépendants qui manquent leur jeu de rêve la nuit. Quand j'étais à Piksonik, il y a eu un cas où le programmeur du serveur a perdu tous les polymères, et notre jeu a été contraint de subir une modération, ayant au lieu du serveur un mannequin défendant stupidement tout l'état du jeu de temps en temps.
Un inconvénient qui, pour une raison quelconque, est systématiquement sous-estimé:
- Si le programmeur client a fait quelque chose de mal et qu'il est invisible pendant le test, par exemple, la probabilité de marchandises dans les boîtes mystérieuses, alors personne ne peut écrire la même chose une deuxième fois et trouver une erreur. Le code autoportable nécessite une attitude beaucoup plus responsable envers les tests.
Informations de débogage détaillées
L'une de nos priorités déclarées est la commodité du débogage. Si lors de l'exécution de l'équipe, nous avons attrapé l'exécution - tout est clair, nous rétrogradons l'état du jeu, envoyons l'état complet aux journaux et sérialisons la commande qui l'a déposé, tout est pratique et beau. La situation est plus compliquée si nous avons une désynchronisation avec le serveur. Parce que le client a déjà exécuté plusieurs autres commandes depuis lors, et il s'avère non seulement de savoir dans quel état était le modèle avant d'exécuter la commande qui a conduit à la catastrophe, mais je le veux vraiment. Cloner un gamestate devant chaque équipe est trop compliqué et trop cher. Pour résoudre le problème, nous compliquons le schéma cousu sous le capot du moteur.
Chez le client, nous n'aurons pas un État gamest, mais deux. Le premier sert d'interface principale pour le rendu, les commandes lui sont appliquées immédiatement. Après cela, les commandes appliquées sont mises en file d'attente pour être envoyées au serveur. Le serveur effectue la même action de son côté et confirme que tout va bien et est correct. Après avoir reçu la confirmation, le client prend la même commande et l'applique au deuxième gamestate, l'amenant à l'état qui a déjà été confirmé par le serveur comme correct. Dans le même temps, nous avons également la possibilité de comparer le hachage des modifications apportées pour être sûr, et nous pouvons également comparer le hachage complet de l'arborescence entière sur le client, que nous pouvons calculer après l'exécution de la commande, il pèse un peu et est considéré comme assez rapide. Si le serveur ne dit pas que tout va bien, il demande au client des détails sur ce qui s'est passé, et le client peut lui envoyer un deuxième état de jeu sérialisé exactement tel qu'il était avant que la commande ne soit exécutée avec succès sur le client.
La solution semble très attrayante, mais elle pose deux problèmes qui doivent être résolus au niveau du code:
- Parmi les paramètres de commande, il peut y avoir non seulement des types simples, mais aussi des liens vers des modèles. Dans un autre État de jeu, au même endroit exact se trouvent d'autres objets du modèle. Nous résolvons ce problème de la manière suivante: Avant l'exécution de la commande sur le client, nous sérialisons toutes ses données. Parmi eux, il peut y avoir des liens vers des modèles, que nous écrirons sous forme de chemin vers le modèle depuis la racine de l'état du jeu. Nous le faisons avant l'équipe, car après son exécution, les chemins peuvent changer. Ensuite, nous envoyons ce chemin au serveur, et le gamestate du serveur pourra obtenir un lien vers son modèle en cours de route. De même, lorsqu'une équipe est appliquée au deuxième état de jeu, le modèle peut être obtenu à partir du deuxième état de jeu.
- En plus des types et modèles élémentaires, une équipe peut avoir des liens vers des collections. Dictionnaire <clé, modèle>, Dictionnaire <modèle, clé>, Liste <Modèle>, Liste <Valeur>. Pour chacun d'eux, ils doivent écrire des sérialiseurs. Certes, vous ne pouvez pas vous précipiter là-dedans, dans un vrai projet, de tels domaines surviennent étonnamment rarement.
- L'envoi de commandes au serveur une à la fois n'est pas une bonne idée, car l'utilisateur peut les produire plus rapidement qu'Internet ne peut les faire glisser d'avant en arrière, sur un Internet médiocre, le pool de commandes non élaboré par le serveur augmentera. Au lieu d'envoyer des commandes une par une, nous les enverrons par lots de plusieurs pièces. Dans ce cas, après avoir reçu une réponse du serveur que quelque chose s'est mal passé, vous devrez d'abord appliquer au deuxième état toutes les commandes précédentes du même package qui ont été confirmées par le serveur, puis seulement effacer et envoyer le deuxième état de contrôle au serveur.
Commodité et facilité d'écriture des commandes
Le code d'exécution des commandes est le deuxième plus grand et le premier code le plus responsable du jeu. Plus ce sera simple et clair, et moins le programmeur aura besoin de faire plus avec ses mains pour l'écrire, plus le code sera écrit rapidement, moins il y aura d'erreurs et, de manière très inattendue, plus le programmeur sera heureux. Je place le code d'exécution directement dans la commande elle-même, en plus des pièces et fonctions générales qui se trouvent dans des classes de règles statiques distinctes, le plus souvent sous la forme d'extensions aux classes de modèle avec lesquelles elles fonctionnent. Je vais vous montrer quelques exemples de commandes de mon projet pour animaux de compagnie, l'une très simple et l'autre un peu plus compliquée:
namespace HexKingdoms { public class FCSetSideCostCommand : HexKingdomsCommand {
Et voici le journal que cette commande laisse après elle-même, si ce journal n'est pas désactivé pour cela.
[FCSetSideCostCommand id=1 match=FCMatchModel[0] newCost=260] Execute:00:00:00.0027546 Apply:00:00:00.0008689 { "LOCAL_PERSISTENTS":{ "@changed":{ "0":{"SIDE_COST":260}, "1":{"POSSIBLE_COST":260}, "2":{"POSSIBLE_COST":260}}}}
La première fois indiquée dans le journal est le temps pendant lequel toutes les modifications nécessaires ont été apportées au modèle, et la seconde est le temps pendant lequel toutes les modifications ont été effectuées par les contrôleurs d'interface. Cela devrait être indiqué dans le journal afin de ne pas faire accidentellement quelque chose de terriblement lent, ou de remarquer à temps si les opérations commencent à prendre trop de temps simplement en raison de la taille du modèle lui-même.
Mis à part les appels à des objets persistants sur Id-shniks, qui réduisent considérablement la lisibilité du journal, ce qui aurait d'ailleurs pu être évité ici, le code de commande lui-même et le journal qu'il a fait avec l'état du jeu sont incroyablement clairs. Veuillez noter que dans le texte de la commande, le programmeur ne fait aucun mouvement supplémentaire. Tout ce dont vous avez besoin est fait par le moteur sous le capot.
Voyons maintenant un exemple d'une plus grande équipe
namespace HexKingdoms { public class FCSetUnitForPlayerCommand : HexKingdomsCommand {
Et voici le journal laissé par l'équipe:
[FCSetUnitForPlayerCommand id=3 screen=/UI_SCREENS[main] unit=militia count=1] Execute:00:00:00.0065625 Apply:00:00:00.0004573 { "LOCAL_PERSISTENTS":{ "@changed":{ "2":{ "UNITS":{ "@set":{"militia":1}}, "ASSIGNED":7}}}, "UI_SCREENS":{ "@changed":{ "main":{ "SELECTED_UNITS":{ "@set":{ "militia":{"@new":null, "TYPE":"militia", "REMARK":null, "COUNT":1, "SELECTED":false, "DISABLED":false, "HIGHLIGHT_GREEN":false, "HIGHLIGHT_RED":false, "BUTTON_ENABLED":false}}}}}}}
Comme on dit, c'est beaucoup plus clair. Prenez le temps d'équiper l'équipe d'un journal pratique, compact et informatif. C'est la clé de votre bonheur. Le modèle doit fonctionner très rapidement, nous y avons donc utilisé une variété d'astuces avec des méthodes de stockage et d'accès aux champs. Les commandes sont exécutées dans le pire des cas une fois par trame, en fait, plusieurs fois moins souvent, nous allons donc effectuer la sérialisation et la désérialisation des champs de commande sans aucune fantaisie, juste par réflexion. Nous ne trions les champs que par nom afin que l'ordre soit fixe, eh bien, nous compilerons la liste des champs une fois pendant la durée de vie de la commande, et nous lirons en utilisant des méthodes natives C #.
Modèle d'information pour l'interface.
Prenons la prochaine étape pour compliquer notre moteur, une étape qui semble effrayante, mais qui simplifie considérablement l'écriture et le débogage des interfaces. Très souvent, en particulier dans le modèle MVP associé, le modèle ne contient que la logique métier contrôlée par le serveur et les informations sur l'état de l'interface sont stockées dans le présentateur. Par exemple, vous souhaitez commander cinq billets. Vous avez déjà sélectionné leur numéro, mais vous n'avez pas encore cliqué sur le bouton "commander". Les informations sur le nombre exact de billets que vous avez choisis dans le formulaire peuvent être stockées quelque part dans les coins secrets de la classe, qui sert de joint entre le modèle et son affichage. Ou, par exemple, le joueur passe d'un écran à un autre, mais rien ne change dans le modèle, et où il se trouvait lorsque la tragédie s'est produite, le programmeur de débogage ne sait que par les mots d'un testeur extrêmement discipliné. L'approche est simple, compréhensible, presque toujours utilisée et un peu malveillante, à mon avis. Parce que si quelque chose a mal tourné, l'état de ce présentateur, qui a conduit à une erreur, est absolument impossible à découvrir. Surtout si l'erreur s'est produite sur le serveur de combat lors de l'opération pour 1000 $, et non sur le testeur dans un environnement contrôlé et reproductible.
Au lieu de cette approche habituelle, nous interdisons à quiconque, à l'exception du modèle, de contenir des informations sur l'état de l'interface. Cela a, comme d'habitude, des avantages et des inconvénients qui doivent être combattus.
- (+1) L'avantage le plus important, économiser des mois de travail de programmation - en cas de problème, le programmeur charge simplement l'état du jeu avant l'accident et reçoit exactement le même état non seulement du modèle commercial, mais de l'interface entière jusqu'au dernier bouton de l'écran.
- (+2) Si une équipe a changé quelque chose dans l'interface, le programmeur peut facilement aller dans le journal et voir exactement ce qui a changé sous une forme json pratique, comme dans la section précédente.
- (-1) Beaucoup d'informations redondantes apparaissent dans le modèle qui ne sont pas nécessaires pour comprendre la logique métier du jeu et ne sont pas nécessaires deux fois par le serveur.
Pour résoudre ce problème, nous marquerons certains champs comme notServerVerified, cela ressemble à ceci, par exemple, comme ceci:
public EDictionary<string, UIStateModel> uiScreens { get { return UI_SCREENS.Get(this); } } public static PDictionaryModel<string, UIStateModel> UI_SCREENS = new PDictionaryModel<string, UIStateModel>() { notServerVerified = true };
Cette partie du modèle et tout ce qui se trouve en dessous concernera exclusivement le client.
Si vous vous souvenez encore, les indicateurs de ce que vous devez exporter et de ce qui ne ressemble pas à ceci:
[Flags] public enum ExportMode { all = 0x0, changes = 0x1, serverVerified = 0x2 }
Par conséquent, lors de l'exportation ou du calcul d'un hachage, vous pouvez spécifier s'il faut exporter la totalité de l'arborescence ou uniquement la partie de celle-ci qui est vérifiée par le serveur.
La première complication évidente qui en résulte est la nécessité de créer des commandes distinctes qui doivent être vérifiées par le serveur et celles qui ne sont pas nécessaires, mais il y a aussi celles qui doivent être vérifiées pas entièrement. Afin de ne pas charger le programmeur avec des opérations inutiles pour configurer la commande, nous essaierons à nouveau de faire tout ce qui est nécessaire avec le capot moteur.
public partial class Command { public virtual void Apply(ModelRoot root) {} public virtual void ApplyClientSide(ModelRoot root) {} }
Le programmeur qui crée la commande peut remplacer l'une ou les deux de ces fonctions. Bien sûr, tout cela est merveilleux, mais comment puis-je m'assurer que le programmeur n'a rien gâché et s'il a gâché quelque chose - comment peut-il l'aider rapidement et facilement à le réparer? Il y a deux façons. J'ai appliqué le premier, mais vous aimerez peut-être davantage le second.
Première voie
Nous utilisons les fonctionnalités intéressantes de notre modèle:
- Le moteur appelle la première fonction, après quoi il reçoit un hachage de changements dans la partie vérifiée par le serveur de l'état du jeu. S'il n'y a pas de changement, nous traitons exclusivement avec l'équipe client.
- Nous obtenons le hachage du modèle des changements dans le modèle entier, pas seulement celui vérifié par le serveur. S'il diffère du hachage précédent, le programmeur a foiré et changé quelque chose dans la partie du modèle qui n'a pas été vérifiée par le serveur. Nous parcourons l'arbre d'état et vidons le programmeur en tant qu'exécution d'une liste complète des champs notServerVerified = true et de ceux situés en dessous de l'arbre qu'il a changé.
- Nous appelons la deuxième fonction. Nous obtenons du modèle un hachage des changements survenus dans la partie vérifiée. S'il ne coïncide pas avec le hachage après le premier appel, alors dans la deuxième fonction, le programmeur a fait n'importe quoi. Si nous voulons obtenir un journal très informatif dans ce cas, nous restaurons le modèle entier à son état d'origine, le sérialisons dans un fichier, puis le programmeur sera utile pour le débogage, puis le clonera en entier (deux lignes - sérialisation-désérialisation), et maintenant nous appliquons d'abord le premier , nous validons les modifications afin que le modèle ne change pas, après quoi nous appliquons la deuxième fonction. Et puis nous exportons toutes les modifications dans la partie vérifiée par le serveur sous la forme de JSON et l'incluons dans l'exécution abusive, afin que le programmeur honteux puisse immédiatement voir ce qu'il a changé et où il a changé, ce qui ne devrait pas être changé.
Cela a l'air bien sûr effrayant, mais en fait c'est 7 lignes, parce que les fonctions qui font tout cela (sauf traverser l'arbre depuis le deuxième paragraphe), nous sommes prêts. Et comme il s'agit de réception, nous pouvons nous permettre d'agir de manière non optimale.
Deuxième voie
Un peu plus brutal, maintenant dans ModelRoot, nous avons un champ de verrouillage, mais nous pouvons le diviser en deux, l'un ne verrouillera que les champs vérifiés sur le serveur, l'autre uniquement les champs vérifiés. Dans ce cas, le programmeur qui a fait quelque chose de mal recevra immédiatement une explication à ce sujet avec un lien avec l'endroit où il l'a fait. Le seul inconvénient de cette approche est que si dans notre arbre une propriété de modèle est marquée comme non vérifiable, alors tout ce qui se trouve dans l'arbre en dessous concernant le calcul des hachages et le contrôle des modifications ne sera pas inspecté, même si chaque champ n'a pas été marqué. Un verrou, bien sûr, ne regardera pas dans la hiérarchie, ce qui signifie que tous les champs de la partie non contrôlée de l'arbre devront être marqués, et il ne fonctionnera pas à certains endroits pour utiliser les mêmes classes dans l'interface utilisateur et la partie habituelle de l'arbre. En option, une telle construction est possible (je l'écrirai simplifiée):
public class GameState : Model { public RootModelData data; public RootModelLocal local; } public class RootModel { public bool locked { get; } }
Il s'avère ensuite que chaque sous-arbre a son propre verrou. GameState hérite des modèles, car il est plus facile que de proposer une implémentation distincte de toutes les mêmes fonctionnalités pour lui.
Améliorations nécessaires
Bien entendu, le responsable du traitement des équipes devra ajouter de nouvelles fonctionnalités. L'essence des modifications sera que toutes les commandes ne seront pas envoyées au serveur, mais uniquement celles qui créent les modifications vérifiées. Le serveur de son côté ne soulèvera pas l'arborescence d'état du jeu entier, mais uniquement la partie vérifiée, et en conséquence le hachage ne coïncidera que pour la partie vérifiée. Lorsqu'une commande est exécutée sur le serveur, seule la première des deux fonctions de la commande sera lancée, et lors de la résolution des références aux modèles dans le gamestate, si le chemin mène à une partie non vérifiable de l'arborescence, null sera placé dans la variable de commande au lieu du modèle. Toutes les équipes non envoyeuses se conformeront honnêtement aux équipes habituelles, mais seront considérées comme déjà confirmées. Dès qu'ils atteignent la ligne et qu'il n'y en a pas non confirmés devant eux, ils seront immédiatement appliqués au deuxième état.
Il n'y a rien de fondamentalement compliqué dans la mise en œuvre. C'est juste que la propriété de chaque champ du modèle a une autre condition, une traversée d'arbre.
Un autre raffinement nécessaire - vous aurez besoin de Factory for ParsistentModel distinct dans les parties vérifiées et non vérifiées de l'arborescence et NextFreeId sera différent pour elles.
Commandes lancées par le serveur
Il y a un problème si le serveur veut envoyer sa commande au client, car l'état du client par rapport au serveur peut déjà avancer de quelques pas. L'idée principale est que si le serveur devait envoyer sa commande, il envoie la notification du serveur au client avec la réponse suivante et l'écrit dans le champ des notifications envoyées à ce client. Le client reçoit une notification, forme une commande sur sa base et la place à la fin de sa file d'attente, après celles qui se sont terminées sur le client mais qui n'ont pas encore atteint le serveur. Après un certain temps, la commande est envoyée au serveur dans le cadre du processus normal de travail avec le modèle. Ayant reçu cette commande pour traitement, le serveur jette la notification hors de la file d'attente sortante. Si le client n'a pas répondu à la notification dans le délai défini avec le package suivant, une commande de redémarrage lui est envoyée. Si le client qui a reçu la notification est tombé, se connecte plus tard ou, pour une raison quelconque, charge le jeu, le serveur transformera toutes les notifications en commandes avant de lui donner l'état, les exécutera de son côté, et ce n'est qu'après cela que le client rejoignant son nouvel état. Veuillez noter qu'un joueur peut avoir un état conflictuel avec des ressources négatives lorsque le joueur a réussi à dépenser l'argent exactement au moment où le serveur les lui a pris. La coïncidence est peu probable, mais avec une grande DAU, elle est presque inévitable. Par conséquent, l'interface et les règles du jeu ne devraient pas tomber à mort dans une telle situation.
Commandes à exécuter dont vous avez besoin de connaître la réponse du serveur
Une erreur typique est de penser qu'un nombre aléatoire ne peut être obtenu qu'à partir du serveur. Rien ne vous empêche d'avoir le même générateur de nombres pseudo-aléatoires fonctionnant simultanément à partir du client et du serveur, à partir d'un sid commun. De plus, la graine actuelle peut être stockée directement dans le gamestate. Certains peuvent trouver difficile de synchroniser la réponse de ce générateur. En fait, pour cela, il suffit d'avoir un numéro de plus dans le même article - exactement combien de numéros ont été reçus du générateur à ce moment. Si votre générateur pour une raison quelconque ne converge pas, alors vous avez quelque part une erreur et le code ne fonctionne pas de manière déterministe. Et ce fait ne doit pas être caché sous le tapis, mais trié et rechercher une erreur. Pour la grande majorité des cas, y compris même les boîtes mystérieuses, cette approche est suffisante.
Cependant, il y a des moments où cette option ne convient pas. Par exemple, vous jouez un prix très cher et ne voulez pas que le camarade rusé décompile le jeu, et écrivez un bot qui vous indique à l'avance ce qui tombera de la boîte de diamants si vous l'ouvrez maintenant, et si vous faites tourner le tambour dans un autre endroit avant cela. Vous pouvez stocker des graines pour chaque variable aléatoire séparément, cela protégera contre le piratage frontal, mais cela n'aidera en rien d'un bot qui vous indique combien de boîtes le produit dont vous avez besoin se trouve actuellement. Eh bien, le cas le plus évident est que vous ne voudrez peut-être pas briller dans la configuration client avec des informations sur la probabilité d'un événement rare. Bref, il est parfois nécessaire d'attendre une réponse du serveur.
De telles situations ne devraient pas être résolues grâce aux capacités supplémentaires du moteur, mais en divisant l'équipe en deux - le premier prépare la situation et met l'interface en attente de notifications, le second en fait, avec la réponse dont vous avez besoin. Même si vous bloquez étroitement l'interface entre eux sur le client, une autre commande peut passer à travers - par exemple, une unité d'énergie sera restaurée à temps.
Il est important de comprendre que de telles situations ne sont pas la règle, mais l'exception. En fait, la plupart des jeux n'ont besoin que d'une seule équipe en attente d'une réponse - GetInitialGameState. Un autre pack de ces commandes est l'interaction entre les joueurs dans un méta-jeu, GetLeaderboard, par exemple. Les deux cents autres pièces sont déterministes.
Stockage des données du serveur et sujet boueux de l'optimisation du serveur
J'avoue tout de suite que je suis un client, et parfois j'ai entendu de telles idées et algorithmes de la part de serveurs familiers qu'ils ne se seraient même pas glissés dans ma tête. En communiquant avec mes collègues, j'ai en quelque sorte développé une image du fonctionnement de mon architecture côté serveur dans le cas idéal. Cependant: il existe des contre-indications, il est nécessaire de consulter un serveur spécialisé.
Tout d'abord sur le stockage des données. C'est votre côté serveur qui peut avoir des restrictions supplémentaires. Par exemple, vous pouvez être interdit d'utiliser des champs statiques. De plus, le code des commandes et des modèles est autoportable, mais le code de propriété sur le client et sur le serveur n'a pas du tout à coïncider. Tout peut y être caché, jusqu'à l'initialisation paresseuse des valeurs de champ du memcache, par exemple. Les champs de propriété peuvent également recevoir des paramètres supplémentaires utilisés par le serveur, mais n'affectent pas le travail du client.
La première différence cardinale du serveur: où les champs sont sérialisés et désérialisés. Une solution raisonnable est que la plupart de l'arbre d'état est sérialisé en un immense champ binaire ou json. Dans le même temps, certains champs sont extraits des tables. Cela est nécessaire car les valeurs de certains champs seront constamment nécessaires au fonctionnement des services d'interaction entre les joueurs. Par exemple, l'icône et le niveau sont constamment secoués par une variété de personnes. Il vaut mieux les conserver dans une base de données régulière. Et l'état complet ou partiel, mais détaillé d'une personne aura très rarement besoin de quelqu'un d'autre que lui, quand quelqu'un décide de se pencher sur son territoire.
De plus, tirer des champs de la base un à la fois n'est pas pratique, et il peut s'avérer que tout traîne pendant longtemps. Une solution très non standard, disponible uniquement pour notre architecture, peut consister dans le fait que le client, lors de l'exécution d'une commande, collecte des informations sur tous les champs stockés séparément dans des tables dont les getters ont réussi à toucher, et ajoute ces informations à la commande pour que le serveur puisse lever ce groupe de champs une demande à la base de données. Bien sûr, avec des restrictions raisonnables, afin de ne pas mendier pour DDOS causé par des programmeurs aux mains courbées qui ont touché à tout de manière inattentive.
Avec un tel stockage séparé, on devrait considérer les mécanismes de transactionnalité lorsqu'un joueur rampe dans les données d'un autre, par exemple, lui vole de l'argent. Mais dans le cas général, nous le faisons par notification. Autrement dit, le voleur reçoit son argent immédiatement, et la personne volée reçoit une notification avec des instructions pour radier de l'argent quand il s'agit de cela.
Comment les équipes sont réparties entre les serveurs
Maintenant, le deuxième moment important pour le serveur. Il existe deux approches. Au début, pour le traitement de toute demande (ou d'un paquet de demandes), l'état entier est élevé de la base de données ou du cache vers la mémoire, traité, puis renvoyé à la base de données. Les opérations sont élaborées de manière atomique sur un tas de serveurs d'exécution différents, et ils n'ont qu'une base commune, et même pas toujours. En tant que client, élever l'état entier à chaque équipe est choquant, mais j'ai vu comment cela fonctionne, et cela fonctionne de manière très fiable et évolutive. La deuxième option est que l'état monte une fois en mémoire et y habite jusqu'à ce que le client ne tombe qu'occasionnellement en ajoutant son état actuel à la base de données. . - . , . , , . 10 . , , — . , . — .
, : , . . . , . , . — — .
, , - . , . , VR CS, - . , , , 30%.
, — , . , . , , , , , .
, , - , : . , . , 35 . , , . , , , — .
: — 30 . ? №1: . №2: , 3000 .
, — . Quelque chose comme ça:
public interface Command { void Apply(ModelRoot root, long time); }
, , Unity — . UnixTime , , PTime, PValue<long> , JSON : - . . .
Quatrième situation: Dans l'état de jeu, il y a des situations où une équipe doit être initiée sans la participation d'un joueur, à temps, par exemple, la récupération d'énergie. Une situation très courante, en fait. Je veux avoir un champ, c'est un exercice pratique. Par exemple PTimeOut, dans lequel il sera possible d'enregistrer un point dans le temps après lequel une commande doit être créée et exécutée. Dans le code, cela peut ressembler à ceci: public class MyModel : Model { public static PTimeOut RESTORE_ENERGY = new PTimeOut() {command = (model, property) => new RestoreEnergyCommand() { model = model}} public long restoreEnergy { get { return RESTORE_ENERGY.Get(this); } set { RESTORE_ENERGY.Set(this, value); }} }
, . , , . , , , , . , , .
- , . , , , , , . , currentTime, :
public partial class Model { public void SetCurrentTime(long time); } vs public partial class RootModel { public event Action<long> setCurrentTime; }
C'est bien, mais le problème est que les modèles qui sont supprimés de l'arborescence des modèles pour toujours et contiennent un tel champ resteront abonnés à cet événement et devront le résoudre correctement. Avant d'envoyer une commande, vérifiez si elles sont toujours dans l'arborescence et ont un lien faible avec cet événement ou inversion de contrôle, de sorte qu'elles ne restent pas inaccessibles au GC.Annexe 1, un cas typique tiré de la vie réelle
. , - . , , . , , , , callback, . , . , , , , « » , , . , , .
, . , inventory, . , , -, , . , « » , , , . « ». , . , , . :
public class OpenMisterBox : Command { public BoxItemModel item; public int slot;
Qu'avons-nous finalement? View, , , . . GameState , , . , , , .
Total
- , , , , , , . . , , . , , .
Et maintenant je vous demande de répondre à des questions très importantes pour moi. Si vous avez des idées sur la façon de faire ce que j'ai mal fait, ou si vous voulez simplement commenter mes réponses, je vous attends dans les commentaires. Une proposition de coopération et des instructions sur de nombreuses erreurs de syntaxe, veuillez en PM.