
Présentation
L'histoire a commencé avec un hackathon basé sur la blockchain. Au début de l'événement, j'ai rencontré un homme qui crée des jeux de société comme passe-temps (j'étais sur le test d'un de ces jeux), nous nous sommes associés et avons trouvé une équipe avec laquelle ils ont «aveuglé» un simple jeu stratégique au cours du week-end. Le hackathon est passé, mais l'enthousiasme est resté. Et nous avons eu l'idée d'un jeu de cartes multijoueur sur le bonheur, la communauté mondiale et les élections.
Dans la série d'articles, nous refléterons notre chemin vers la création d'un jeu, avec une description du rake sur lequel nous avons déjà marché et nous avancerons à mesure que nous progressons.
Sous la coupe sera:
- Résumé du jeu
- Comment la décision a été prise sur quoi faire backend. Où va-t-il «vivre» pour ne pas payer pour cela au stade de développement
- Premières étapes du développement - authentification des joueurs et organisation du matchmaking
- Plans supplémentaires
Quel est le jeu
L'humanité est fatiguée des guerres mondiales, de l'épuisement des ressources et de la concurrence constante. Les factions clés ont convenu d'utiliser la technologie moderne pour sélectionner une seule direction. À l'heure fixée, l'électorat mondial doit décider du choix d'une fraction qui gouvernera la planète pour le prochain millénaire. Les factions clés s'engagent dans une lutte de pouvoir «honnête». Dans une session de jeu, chaque joueur représente une fraction.
Ce jeu de cartes concerne les élections. Chaque faction a un budget pour mener la course aux élections, des sources de revenus augmentant le budget et commençant les votes. Au début du jeu, le jeu de cartes d'action est mélangé et 4 cartes sont délivrées à chaque participant. Chaque tour, les joueurs peuvent effectuer jusqu'à deux actions de jeu. Pour utiliser la carte, le joueur la met sur la table et, si nécessaire, désigne l'objectif et déduit du budget le coût d'utilisation de la carte. Après la fin du tour, le joueur ne peut conserver qu'une seule des cartes inutilisées. Au début de chaque tour, les joueurs reçoivent des cartes du jeu, de sorte qu'au début de chaque tour, chaque joueur a 4 cartes action en main.
À la fin des tours 3, 6 et 9, le joueur avec le moins de votes est retiré du jeu. Si plusieurs joueurs ont le même nombre minimum de votes, alors tous les joueurs avec ce résultat sont éliminés du jeu. Les voix de ces joueurs vont au pool général de l'électorat.
À la fin du tour 12, le vainqueur est celui qui a le plus de votes.
Choisir un outil pour le backend
De la description du jeu suit:
- C'est multijoueur
- Il faut en quelque sorte identifier les joueurs et gérer les comptes
- La présence d'une composante sociale bénéficierait au jeu - amis, communautés (clans), chats, réalisations (réalisations)
- Des classements et des fonctionnalités de matchmaking seront nécessaires.
- La fonctionnalité de gestion des tournois sera utile à l'avenir
- Étant donné que le jeu est un jeu de cartes, vous devez gérer le catalogue de cartes, vous devrez peut-être stocker les cartes disponibles pour le joueur et les decks compilés
- À l'avenir, une économie dans le jeu pourrait être nécessaire, y compris de la monnaie dans le jeu, l'échange de biens virtuels (cartes)
En regardant la liste des besoins, je suis immédiatement arrivé à la conclusion que créer mon propre backend au stade initial n'a pas de sens et je suis allé sur google quelles sont les autres options. J'ai donc découvert qu'il existe des backends de jeux cloud spécialisés, parmi lesquels PlayFab (acheté par Microsoft) et GameSparks (acheté par Amazon) se démarquent.
En général, ils sont fonctionnellement similaires et couvrent les besoins de base. De plus, leur architecture interne est très différente, les mêmes tâches sont résolues un peu différemment et les correspondances explicites dans les fonctionnalités sont difficiles à tracer. Vous trouverez ci-dessous les caractéristiques positives et négatives de chaque plate-forme et des considérations sur le sujet de choix.
Playfab
Caractéristiques positives:
- Les comptes de différents jeux sont combinés en un compte principal
- L'économie du jeu est décrite sans une seule ligne de code, y compris les prix vers un magasin virtuel séparé
- Interface utilisateur conviviale
- Microsoft acquiert un produit après son acquisition
- Le coût de possession en production par abonnement Indie Studio est de 99 $ (jusqu'à 100k MAU), lors du passage à Professional, un abonnement de 1k MAU coûtera 8 $ (compte minimum 300 $)
Caractéristiques négatives:
- Le stockage des données de jeu est strictement limité, par exemple, dans un abonnement gratuit pour stocker des données pour une session de jeu spécifique (si je comprends bien, des groupes d'entités sont utilisés pour cela) 3 emplacements de 500 octets chacun sont disponibles
- Pour organiser le multijoueur, vous devez connecter des serveurs tiers qui traiteront les événements des clients et calculeront la logique du jeu. Il s'agit de Photon sur votre matériel ou d'Azure Thunderhead, et vous devez non seulement organiser le serveur, mais également mettre à niveau votre abonnement vers au moins Indie Studio
- Il est nécessaire de supporter le fait que le code cloud sans saisie semi-automatique et il n'y a aucun moyen de se diviser en modules (ou ne pas trouver?)
- Il n'y a pas de débogueur normal, vous pouvez uniquement écrire des journaux dans CloudScript et afficher
Gamesparks
Caractéristiques positives:
- Stockage des données du jeu. Non seulement il existe de nombreux endroits où vous pouvez enregistrer des données (métadonnées générales de jeu, biens virtuels, profil de joueur, sessions multijoueurs, etc.), la plate-forme fournit également une base de données à part entière en tant que service qui n'est attachée à rien, de plus, MongoDB et Redis sont disponibles immédiatement pour différents types de données. Dans l'environnement de développement, vous pouvez stocker 10 Mo, dans la bataille 10 Go
- Le multijoueur est disponible dans un abonnement gratuit (développement) avec une limite de 10 connexions simultanées et 10 requêtes par seconde
- Travail pratique avec CloudCode, y compris un outil intégré pour tester et déboguer (Test Harness)
Caractéristiques négatives:
- Le sentiment que depuis l'achat par Amazon (hiver 2018) l'outil stagne, il n'y a pas d'innovations
- Encore une fois, après l'acquisition d'Amazon, les tarifs ont empiré; auparavant, il était possible d'utiliser jusqu'à 10000 MAU en production gratuitement
- Le coût de possession de production commence à 300 $ (abonnement standard)
Réflexions
Vous devez d'abord vérifier le concept du jeu. Pour ce faire, je veux construire un prototype de bâtons et de scotch sans investissements monétaires et commencer à tester les mécanismes de jeu. Par conséquent, en premier lieu lors du choix, je profite de l'occasion pour développer et tester un mécanicien sur un abonnement gratuit.
GameSparks satisfait à ce critère, mais PlayFab ne le fait pas, car vous aurez besoin d'un serveur qui gérera les événements des clients de jeu et d'un abonnement Indie studio (99 $).
Dans le même temps, j'accepte le risque qu'Amazon ne développe pas de GameSparks, ce qui signifie qu'il peut «mourir». Compte tenu de cela et toujours du coût de possession en production, j'ai à l'esprit la nécessité potentielle de passer soit à une autre plateforme soit à mon propre backend.
Premières étapes de développement
Connexion et authentification
Ainsi, le choix s'est porté sur GameSparks en tant que backend au stade du prototypage. La première étape consiste à apprendre à se connecter à la plateforme et à authentifier le joueur. Un point important est que l'utilisateur doit pouvoir jouer sans inscription ni SMS immédiatement après l'installation du jeu. Pour ce faire, GameSparks offre la possibilité de créer un profil anonyme en appelant la méthode DeviceAuthenticationRequest, plus tard sur la base d'un profil anonyme, vous pouvez en créer un à part entière en vous connectant, par exemple, avec votre compte Google.
Étant donné que j'ai un cerveau TDD, j'ai commencé par créer un test pour connecter le client au jeu. Puisqu'à l'avenir, CloudCode devra être écrit en JS, je ferai des tests d'intégration en JS en utilisant mocha.js et chai.js. Le premier test s'est avéré comme ceci:
var expect = require("chai").expect; var GameClientModule = require("../src/gameClient"); describe("Integration test", function () { this.timeout(0); it("should connect client to server", async function () { var gameClient = new GameClientModule.GameClient(); expect(gameClient.connected()).is.false; await gameClient.connect(); expect(gameClient.connected()).is.true; }); })
Par défaut, le délai d'attente dans mocha.js est de 2 secondes, je le fais immédiatement sans fin, car les tests sont l'intégration. Dans le test, je crée un client de jeu qui n'a pas encore été implémenté, vérifie qu'il n'y a pas de connexion au serveur, appelle la commande pour se connecter au backend et vérifie que le client s'est correctement connecté.
Pour que le test devienne vert, vous devez télécharger et ajouter le SDK GameSparks JS au projet, ainsi que connecter ses dépendances (crypto-js et ws), et, bien sûr, implémenter GameClientModule:
var GameSparks = require("../gamesparks-javascript-sdk-2018-04-18/gamesparks-functions"); var config = new require("./config.json"); exports.GameClient = function () { var gamesparks = new GameSparks(); this.connected = () => (gamesparks.connected === true); this.connect = function () { return new Promise(function (resolve, reject) { gamesparks.initPreview({ key: config.gameApiKey, secret: config.credentialSecret, credential: config.credential, onInit: () => resolve(), onMessage: onMessage, onError: (error) => reject(error), logger: console.log }); }); } function onMessage(message) { console.log("GAME onMessage: " + JSON.stringify(message)); } }
Au démarrage de l'implémentation du client de jeu, les clés nécessaires à l'autorisation technique pour créer une connexion à partir de l'application client sont lues dans la configuration. La méthode connectée enveloppe le même champ à partir du SDK. La chose la plus importante se produit dans la méthode connect, elle renvoie une promesse avec des rappels pour une connexion réussie ou une erreur, lie également le gestionnaire onMessage au même rappel. onMessage agira en tant que gestionnaire de traitement des messages depuis le backend, pour l'instant laissez-le enregistrer les messages sur la console.
Il semblerait que le travail soit terminé, mais le test reste rouge. Il s'avère que le SDK GameSparks JS ne fonctionne pas avec node.js; pour lui, vous voyez, il manque le contexte du navigateur. Faisons-lui penser que le nœud est Chrome sur le coquelicot. Nous allons sur gamesparks.js et au tout début nous ajoutons:
if (typeof module === 'object' && module.exports) {
Le test est devenu vert, passant à autre chose.
Comme je l'ai écrit plus tôt, un joueur devrait pouvoir commencer à jouer immédiatement dès qu'il entre dans le jeu, alors que je veux commencer à accumuler des analyses en activité. Pour ce faire, nous nous lions soit à l'identifiant de l'appareil, soit à un identifiant généré aléatoirement. Vérifiez que ce sera un tel test:
it("should auth two anonymous players", async function () { var gameClient1 = new GameClientModule.GameClient(); expect(gameClient1.playerId).is.undefined; var gameClient2 = new GameClientModule.GameClient(); expect(gameClient2.playerId).is.undefined; await gameClient1.connect(); await gameClient1.authWithCustomId("111"); expect(gameClient1.playerId).is.equals("5b5f5614031f5bc44d59b6a9"); await gameClient2.connect(); await gameClient2.authWithCustomId("222"); expect(gameClient2.playerId).is.equals("5b5f6ddb031f5bc44d59b741"); });
J'ai décidé de vérifier immédiatement 2 clients afin de m'assurer que chaque client crée son propre profil sur le backend. Pour ce faire, le client du jeu aura besoin d'une méthode dans laquelle vous pouvez transférer un certain identifiant externe à GameSparks, puis vérifier que le client a contacté le profil de joueur souhaité. Profils préparés à l'avance sur le portail GameSparks.
Pour l'implémentation dans GameClient, ajoutez:
this.playerId = undefined; this.authWithCustomId = function (customId) { return new Promise(resolve => { var requestData = { "deviceId": customId , "deviceOS": "NodeJS" } sendRequest("DeviceAuthenticationRequest", requestData) .then(response => { if (response.userId) { this.playerId = response.userId; resolve(); } else { reject(new Error(response)); } }) .catch(error => { console.error(error); }); }); } function sendRequest(requestType, requestData) { return new Promise(function (resolve) { gamesparks.sendWithData(requestType, requestData, (response) => resolve(response)); }); }
La mise en œuvre revient à envoyer une demande DeviceAuthenticationRequest, à recevoir l'identifiant du joueur de la réponse et à le placer dans la propriété du client. Immédiatement, dans une méthode distincte, l'aide a envoyé des demandes à GameSparks avec un wrapper dans une promis.
Les deux tests sont verts, il reste à ajouter la fermeture de la connexion et le refactor.
Dans GameClient, j'ai ajouté une méthode qui ferme la connexion au serveur (déconnecter) et connectAsAnonymous combinant connect et authWithCustomId. D'une part, connectAsAnonymous viole le principe de la responsabilité unique, mais ne semble pas violer ... En même temps, il ajoute la convivialité, car dans les tests, il sera souvent nécessaire d'authentifier les clients. Qu'en pensez-vous?
Dans les tests, il a ajouté un assistant de méthode d'usine qui crée une nouvelle instance du client de jeu et ajoute au tableau des clients créés. Dans le gestionnaire mocha spécial, après chaque test en cours d'exécution pour les clients du tableau, j'appelle la méthode de déconnexion et efface ce tableau. Je n'aime pas encore les "chaînes magiques" dans le code, j'ai donc ajouté un dictionnaire avec des identifiants personnalisés utilisés dans les tests.
Le code final peut être consulté dans le référentiel, un lien auquel je donnerai à la fin de l'article.
Organisation de recherche de jeux (matchmaking)
Je vais lancer la fonction de matchmaking, qui est très importante pour le multijoueur. Ce système commence à fonctionner lorsque nous appuyons sur le bouton «Rechercher un jeu» dans le jeu. Elle prend des rivaux, des coéquipiers, ou les deux (selon le jeu). En règle générale, dans de tels systèmes, chaque joueur a un indicateur numérique MMR (Match Making Ratio) - une évaluation personnelle du joueur, qui est utilisée pour sélectionner d'autres joueurs avec le même niveau de compétence.
Pour tester cette fonctionnalité, j'ai proposé le test suivant:
it("should find match", async function () { var gameClient1 = newGameClient(); var gameClient2 = newGameClient(); var gameClient3 = newGameClient(); await gameClient1.connectAsAnonymous(playerCustomIds.id1); await gameClient2.connectAsAnonymous(playerCustomIds.id2); await gameClient3.connectAsAnonymous(playerCustomIds.id3); await gameClient1.findStandardMatch(); expect(gameClient1.state) .is.equals(GameClientModule.GameClientStates.MATCHMAKING); await gameClient2.findStandardMatch(); expect(gameClient2.state) .is.equals(GameClientModule.GameClientStates.MATCHMAKING); await gameClient3.findStandardMatch(); expect(gameClient3.state) .is.equals(GameClientModule.GameClientStates.MATCHMAKING); await sleep(3000); expect(gameClient1.state) .is.equals(GameClientModule.GameClientStates.CHALLENGE); expect(gameClient1.challenge, "challenge").is.not.undefined; expect(gameClient1.challenge.challengeId).is.not.undefined; expect(gameClient2.state) .is.equals(GameClientModule.GameClientStates.CHALLENGE); expect(gameClient2.challenge.challengeId) .is.equals(gameClient1.challenge.challengeId); expect(gameClient3.state) .is.equals(GameClientModule.GameClientStates.CHALLENGE); expect(gameClient3.challenge.challengeId) .is.equals(gameClient1.challenge.challengeId); });
Trois clients sont connectés au jeu (à l'avenir, c'est un minimum nécessaire pour vérifier certains scénarios) et sont enregistrés pour rechercher le jeu. Après avoir enregistré le 3ème joueur sur le serveur, une session de jeu est formée et les joueurs doivent s'y connecter. En même temps, l'état des clients change et le contexte de la session de jeu avec le même identifiant apparaît.
Tout d'abord, préparez le backend. Dans GameSparks, il existe un outil prêt à l'emploi pour personnaliser la recherche de jeux, disponible sur le chemin «Configurateur-> Matchs». J'en crée un nouveau et je continue la configuration. En plus des paramètres standard tels que le code, le nom et la description du match, le nombre minimum et maximum de joueurs requis pour un mode de jeu personnalisé est indiqué. Je vais attribuer le code «StandardMatch» au match créé et indiquer le nombre de joueurs de 2 à 3.
Vous devez maintenant configurer les règles de sélection des joueurs dans la section «Seuils». Pour chaque seuil, le temps de son action, le type (absolu, relatif et en pourcentage) et les limites sont indiqués.

Supposons qu'un joueur avec un MMR de 19 commence la recherche. Dans l'exemple ci-dessus, les 10 premières secondes seront la sélection d'autres joueurs avec un MMR de 19 à 21. Si les joueurs ne peuvent pas être sélectionnés, la deuxième bordure de recherche est activée, ce qui étend la plage de recherche de 16 pour les 20 secondes suivantes ( 19-3) à 22 (19 + 3). Ensuite, le troisième seuil est inclus, dans lequel une recherche sera effectuée pendant 30 secondes dans la plage de 14 (19-25%) à 29 (19 + 50%), tandis que le match est considéré comme terminé si le nombre minimum requis de joueurs a été accumulé (Accepter la marque Min . Joueurs).
En fait, le mécanisme est plus compliqué, car il prend en compte le MMR de tous les joueurs qui ont réussi à rejoindre un match particulier. J'analyserai ces détails lorsque le moment sera venu de faire le mode de notation du jeu (pas dans cet article). Pour le mode de jeu standard, où je ne prévois pas encore utiliser MMR, je n'ai besoin que d'un seul seuil.
Lorsque tous les joueurs ont été sélectionnés, vous devez créer une session de jeu et y connecter les joueurs. Dans GameSparks, la fonction de session de jeu est le «défi». Dans le cadre de cette entité, les données de session de jeu sont stockées et des messages sont échangés entre les clients de jeu. Pour créer un nouveau type de Challenge, vous devez suivre le chemin "Configurateur-> Challenges". Là, j'ajoute un nouveau type avec le code "StandardChallenge" et indique que ce type de session de jeu est au tour par tour, c'est-à-dire les joueurs se relaient, pas simultanément. GameSparks prend en même temps le contrôle de la séquence des mouvements.
Pour qu'un client puisse s'inscrire pour rechercher un jeu, vous pouvez utiliser une demande du type MatchmakingRequest, mais je ne le recommanderais pas, car la valeur MMR du joueur est requise comme l'un des paramètres. Cela peut conduire à une fraude de la part du client du jeu, et le client ne doit connaître aucun MMR, il s'agit d'une activité de backend. Pour m'inscrire correctement à la recherche de jeu, je crée un événement arbitraire à partir du client. Cela se fait dans la section «Configurateur-> Événements». J'appelle l'événement FindStandardMatch sans attributs. Maintenant, vous devez configurer la réaction à cet événement, pour cela, je vais dans la section "Configurateur-> Code Cloud" du code cloud, j'écris le gestionnaire suivant pour FindStandardMatch dans la section "Événements":
var matchRequest = new SparkRequests.MatchmakingRequest(); matchRequest.matchShortCode = "StandardMatch"; matchRequest.skill = 0; matchRequest.Execute();
Ce code enregistre un joueur dans StandardMatch avec un MMR de 0, donc tous les joueurs enregistrés pour rechercher un jeu standard conviendront pour créer une session de jeu. Dans la sélection d'un match de classement, il pourrait y avoir un appel aux données privées du profil du joueur pour obtenir le MMR de ce type de match.
Lorsqu'il y a suffisamment de joueurs pour démarrer une session de jeu, GameSparks enverra un message MatchFoundMessage à tous les joueurs sélectionnés. Ici, vous pouvez générer automatiquement une session de jeu et y ajouter des joueurs. Pour ce faire, dans le "Messages utilisateur-> MatchFoundMessage" ajoutez le code:
var matchData = Spark.getData(); if (Spark.getPlayer().getPlayerId() != matchData.participants[0].id) { Spark.exit(); } var challengeCode = ""; var accessType = "PRIVATE"; switch (matchData.matchShortCode) { case "StandardMatch": challengeCode = "StandardChallenge"; break; default: Spark.exit(); } var createChallengeRequest = new SparkRequests.CreateChallengeRequest(); createChallengeRequest.challengeShortCode = challengeCode; createChallengeRequest.accessType = accessType; var tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); createChallengeRequest.endTime = tomorrow.toISOString(); createChallengeRequest.usersToChallenge = []; var participants = matchData.participants; var numberOfPlayers = participants.length; for (var i = 1; i < numberOfPlayers; i++) { createChallengeRequest.usersToChallenge.push(participants[i].id) } createChallengeRequest.Send();
Le code vérifie d'abord qu'il s'agit du premier joueur sur la liste des participants. Ensuite, au nom du premier joueur, une instance de StandardChallenge est créée et les joueurs restants sont invités. Les joueurs invités reçoivent un message ChallengeIssuedMessage. Ici, vous pouvez imaginer le comportement lorsqu'une invitation à rejoindre le jeu s'affiche sur le client et nécessite une confirmation en envoyant AcceptChallengeRequest, ou vous pouvez accepter l'invitation en mode silencieux. Je vais donc le faire, pour cela dans "Messages utilisateur-> ChallengeIssuedMessage" j'ajouterai le code suivant:
var challangeData = Spark.getData(); var acceptChallengeRequest = new SparkRequests.AcceptChallengeRequest(); acceptChallengeRequest.challengeInstanceId = challangeData.challenge.challengeId; acceptChallengeRequest.message = "Joining"; acceptChallengeRequest.SendAs(Spark.getPlayer().getPlayerId());
L'étape suivante, GameSparks distribue l'événement ChallengeStartedMessage. Le gestionnaire global de cet événement ("Messages globaux-> ChallengeStartedMessage") est un endroit idéal pour initialiser une session de jeu, je m'en occupe lors de l'implémentation de la logique du jeu.
Le moment est venu pour l'application cliente. Changements dans le module client:
exports.GameClientStates = { IDLE: "Idle", MATCHMAKING: "Matchmaking", CHALLENGE: "Challenge" } exports.GameClient = function () { this.state = exports.GameClientStates.IDLE; this.challenge = undefined; function onMessage(message) { switch (message["@class"]) { case ".MatchNotFoundMessage": this.state = exports.GameClientStates.IDLE; break; case ".ChallengeStartedMessage": this.state = exports.GameClientStates.CHALLENGE; this.challenge = message.challenge; break; default: console.log("GAME onMessage: " + JSON.stringify(message)); } } onMessage = onMessage.bind(this); this.findStandardMatch = function () { var eventData = { eventKey: "FindStandardMatch" } return new Promise(resolve => { sendRequest("LogEventRequest", eventData) .then(response => { if (!response.error) { this.state = exports.GameClientStates.MATCHMAKING; resolve(); } else { console.error(response.error); reject(new Error(response)); } }) .catch(error => { console.error(error); reject(new Error(error)); }); }); } }
Conformément au test, deux champs sont apparus sur le client - état et défi. La méthode onMessage a acquis un aspect significatif et répond désormais aux messages concernant le début d'une session de jeu et à un message indiquant qu'il n'était pas possible de prendre un jeu. La méthode findStandardMatch a également été ajoutée, qui envoie la requête correspondante au backend. Le test est vert, mais je suis satisfait, la sélection de jeux maîtrisée.
Et ensuite?
Dans les articles suivants, je décrirai le processus de développement de la logique de jeu, de l'initialisation d'une session de jeu au traitement des mouvements. J'analyserai les caractéristiques du stockage de différents types de données - une description des métadonnées du jeu, des caractéristiques du monde du jeu, des données des sessions de jeu et des données sur les joueurs. La logique du jeu sera développée à travers deux types de tests - unitaire et d'intégration.
Je téléchargerai les sources sur github dans des parties liées aux articles.
Il est entendu que pour progresser efficacement dans la création d'un jeu, vous devez élargir notre équipe de passionnés. L'artiste / designer se joindra bientôt. Et le gourou, par exemple, Unity3D, qui fera la tête des plates-formes mobiles, reste à trouver.