Fermer

octobre 22, 2021

Comment créer un jeu multiutilisateurs en temps réel à partir de zéro


Résumé rapide ↬

Cet article met en évidence le processus, les décisions techniques et les enseignements tirés de la création du jeu en temps réel Autowuzzler. Apprenez à partager l'état du jeu entre plusieurs clients en temps réel avec Colyseus, effectuez des calculs physiques avec Matter.js, stockez des données dans Supabase.io et créez le front-end avec SvelteKit.

Alors que la pandémie persistait, l'équipe soudainement éloignée avec laquelle je travaille est devenue de plus en plus privée de baby-foot. J'ai réfléchi à la façon de jouer au baby-foot dans un environnement distant, mais il était clair que simplement reconstituer les règles du baby-foot sur un écran ne serait pas très amusant. balle à l'aide de petites voitures – une réalisation réalisée alors que je jouais avec mon enfant de 2 ans. La même nuit, j'ai entrepris de construire le premier prototype d'un jeu qui deviendrait Autowuzzler.

L'idée est simple : les joueurs dirigent des petites voitures virtuelles dans une arène descendante qui ressemble à une table de baby-foot. La première équipe à marquer 10 buts gagne.

Bien sûr, l'idée d'utiliser des voitures pour jouer au football n'est pas unique, mais deux idées principales devraient distinguer Autowuzzler : je voulais reconstituer une partie du look et la sensation de jouer sur une table de baby-foot physique, et je voulais m'assurer qu'il est aussi facile que possible d'inviter des amis ou des coéquipiers à un jeu occasionnel rapide.

Dans cet article, je décrirai le processus derrière la création d'Autowuzzlerquels outils et frameworks j'ai choisis, et partage quelques détails de mise en œuvre et leçons que j'ai apprises.

Interface utilisateur du jeu montrant un fond de table de baby-foot, six voitures dans deux équipes et une balle.

Autowuzzler (beta) avec six joueurs simultanés dans deux équipes. ( Grand aperçu)

Premier prototype fonctionnel (terrible)

Le premier prototype a été construit à l'aide du moteur de jeu open source Phaser.jsprincipalement pour le moteur physique inclus et parce que j'en avais déjà une certaine expérience. L'étape de jeu a été intégrée dans une application Next.jsencore une fois parce que j'avais déjà une solide compréhension de Next.js et que je voulais me concentrer principalement sur le jeu.

Comme le jeu doit prend en charge plusieurs joueurs en temps réel j'ai utilisé Express en tant que courtier WebSockets. C'est ici que cela devient cependant délicat.

Comme les calculs physiques ont été effectués sur le client dans le jeu Phaser, j'ai choisi une logique simple, mais manifestement erronée : le premier client connecté avait le privilège douteux. de faire les calculs physiques pour tous les objets du jeu, en envoyant les résultats au serveur express, qui à son tour a diffusé les positions, angles et forces mis à jour aux clients de l'autre joueur. Les autres clients appliqueraient ensuite les modifications aux objets du jeu.

Cela a conduit à la situation où le premier joueur a pu voir la physique se dérouler en temps réel (c'est se passe localement dans leur navigateur, après tout), alors que tous les autres joueurs étaient en retard d'au moins 30 millisecondes (le taux de diffusion que j'ai choisi), ou – si la connexion réseau du premier joueur était lente – considérablement pire.

Si cela vous semble une mauvaise architecture, vous avez tout à fait raison. Cependant, j'ai accepté ce fait en faveur d'obtenir rapidement quelque chose de jouable pour déterminer si le jeu est réellement amusant à jouer.

Valider l'idée, jeter le prototype

, il était suffisamment jouable pour inviter des amis pour un premier essai routier. Les commentaires ont été très positifs la principale préoccupation étant, sans surprise, la performance en temps réel. D'autres problèmes inhérents comprenaient la situation lorsque le premier joueur (rappelez-vous, celui qui était en charge de tout) a quitté le jeu – qui devrait prendre le relais ? À ce stade, il n'y avait qu'une seule salle de jeu, donc tout le monde pouvait rejoindre la même partie. J'étais également un peu préoccupé par la taille du paquet introduit par la bibliothèque Phaser.js.

Il était temps de vider le prototype et de commencer avec une nouvelle configuration et un objectif clair.

Configuration du projet

De toute évidence, le " L'approche « first client rules all » devait être remplacée par une solution dans laquelle l'état du jeu réside sur le serveur . Dans mes recherches, je suis tombé sur Colyseusqui semblait être l'outil parfait pour le travail.

Pour les autres blocs de construction principaux du jeu, j'ai choisi :

  • Matter.js comme un moteur physique au lieu de Phaser.js car il s'exécute dans Node et Autowuzzler ne nécessite pas de framework de jeu complet. en version bêta publique à ce moment-là. (En plus : j'adore travailler avec Svelte.)
  • Supabase.io pour stocker les codes PIN de jeu créés par l'utilisateur.

Regardons ces blocs de construction plus en détail.

Plus après le saut ! Continuez à lire ci-dessous ↓

Synchronized, Centralized Game State With Colyseus

Colyseus est un framework de jeu multijoueur basé sur Node.js et Express. À la base, il fournit :

  • Synchroniser l'état entre les clients de manière faisant autorité ;
  • Communication en temps réel efficace à l'aide de WebSockets en envoyant uniquement les données modifiées ;
  • Configurations multi-pièces ;
  • Bibliothèques clientes pour JavaScript , Unity, Defold Engine, Haxe, Cocos Creator, Construct3;
  • Hooks de cycle de vie, par exemple la salle est créée, l'utilisateur rejoint, l'utilisateur quitte, et plus encore ;
  • Envoi de messages, soit sous forme de messages diffusés à tous les utilisateurs de la salle, soit à un seul utilisateur ;
  • Un panneau de surveillance intégré et un outil de test de charge.

Remarque : Les Colyseus docs facilitent la mise en route d'un serveur Colyseus barebones en fournissant un script npm init et un référentiel d'exemples.

Création d'un schéma

L'entité principale d'une application Colyseus est la salle de jeu, qui contient l'état d'une seule instance de salle et de tous ses objets de jeu. Dans le cas d'Autowuzzleril s'agit d'une session de jeu avec :

  • deux équipes,
  • un nombre fini de joueurs,
  • une balle.

Un schéma doit être défini pour tous propriétés des objets de jeu qui devraient être synchronisées entre les clients. Par exemple, nous voulons que la balle se synchronise, et nous devons donc créer un schéma pour la balle :

class Ball Extends Schema {
  constructeur() {
   super();
   this.x = 0;
   this.y = 0;
   this.angle = 0;
   this.velocityX = 0;
   this.vitesseY = 0 ;
  }
}
defineTypes(Boule, {
  x : "nombre",
  y : "nombre",
  angle: "nombre",
  vitesseX : "nombre",
  vitesseY : "nombre"
});

Dans l'exemple ci-dessus, une nouvelle classe qui étend la classe de schéma fournie par Colyseus est créée ; dans le constructeur, toutes les propriétés reçoivent une valeur initiale. La position et le mouvement de la balle sont décrits à l'aide des cinq propriétés : xyanglevelocityX, velocityY . De plus, nous devons spécifier les types de chaque propriété. Cet exemple utilise la syntaxe JavaScript, mais vous pouvez également utiliser la syntaxe TypeScript légèrement plus compacte.

Les types de propriété peuvent être des types primitifs :

  • string
  • boolean
  • number (ainsi que des types entiers et flottants plus efficaces)

ou des types complexes :

  • ArraySchema (similaire à Array en JavaScript)
  • MapSchema (similaire à Map en JavaScript)
  • SetSchema (similaire à Set en JavaScript)
  • CollectionSchema (similaire à ArraySchema, mais sans contrôle sur les index)

La classe Ball ci-dessus possède cinq propriétés de type number : ses coordonnées (xy) , son angle actuel et le vecteur vitesse (velocityXvelocityY).

Le schéma a pour les joueurs est similaire, mais inclut quelques propriétés supplémentaires pour stocker le nom du joueur et le numéro de l'équipe, qui doivent être fournis lors de la création d'une instance Player :

class Player extend Schema {
  constructeur (numéro d'équipe) {
    super();
    this.nom = "";
    this.x = 0;
    this.y = 0;
    this.angle = 0;
    this.velocityX = 0;
    this.vitesseY = 0 ;
    this.teamNumber = teamNumber;
  }
}
defineTypes(Player, {
  nom : "chaîne",
  x : "nombre",
  y : "nombre",
  angle: "nombre",
  vitesseX : "nombre",
  vitesseY : "nombre",
  angularVelocity : "nombre",
  teamNumber: "numéro",
});

Enfin, le schéma de l'Autowuzzler Room connecte les classes précédemment définies : une instance de room a plusieurs équipes (stockées dans un ArraySchema). Il contient également une seule balle, nous créons donc une nouvelle instance de Ball dans le constructeur de RoomSchema. Les joueurs sont stockés dans un MapSchema pour une récupération rapide à l'aide de leurs identifiants.

class RoomSchema extend Schema {
 constructeur() {
   super();
   this.teams = new ArraySchema();
   this.ball = new Ball();
   this.players = new MapSchema();
 }
}
defineTypes(RoomSchema, {
 équipes : [Team]// un tableau d'équipes
 ball : Ball, // une seule instance de Ball
 joueurs : { map: Player } // une carte des joueurs
});
Remarque : La définition de la classe Team est omise.

Configuration multi-pièces ("Match-Making")

Tout le monde peut rejoindre un Autowuzzler jeu s'ils ont un code PIN de jeu valide. Notre serveur Colyseus crée une nouvelle instance de salle pour chaque session de jeu dès que le premier joueur rejoint la salle et supprime la salle lorsque le dernier joueur la quitte.

Le processus d'affectation des joueurs à la salle de jeu souhaitée est appelé « matchmaking ». Colyseus facilite la configuration en utilisant la méthode filterBy lors de la définition d'une nouvelle salle :

gameServer.define("autowuzzler", AutowuzzlerRoom).filterBy(['gamePIN']);

Désormais, tous les joueurs rejoignant le jeu avec le même gamePIN (nous verrons comment « rejoindre » plus tard) se retrouveront dans la même salle de jeu ! Toutes les mises à jour d'état et autres messages diffusés sont limités aux joueurs dans la même pièce. mais laisse au développeur le soin de créer les mécanismes de jeu réels, y compris la physique. Phaser.js, que j'ai utilisé dans le prototype, ne peut pas être exécuté dans un environnement sans navigateur, mais le moteur physique intégré de Phaser.js Matter.js peut s'exécuter sur Node.js.

Avec Matter.js, vous définissez un monde de la physique avec certaines propriétés physiques comme sa taille et sa gravité. Il fournit plusieurs méthodes pour créer des objets physiques primitifs qui interagissent les uns avec les autres en adhérant aux lois (simulées) de la physique, notamment la masse, les collisions, le mouvement avec friction, etc. Vous pouvez déplacer des objets en appliquant une force — comme vous le feriez dans le monde réel.

Un « monde » Matter.js est au cœur du jeu Autowuzzler ; il définit à quelle vitesse les voitures se déplacent, à quel point la balle doit rebondir, où se trouvent les buts et ce qui se passe si quelqu'un tire un but.

let ball = Bodies.circle(
 balleInitialXPosition,
 balleInitialYPposition,
 rayon,
 {
   rendu : {
     lutin : {
       texture : '/assets/ball.png',
     }
   },
   frottement : 0,002,
   restitution : 0,8
 }
);
World.add(this.engine.world, [ball]);

Code simplifié pour ajouter un objet de jeu « balle » à la scène dans Matter.js.

Une fois les règles définies, Matter.js peut s'exécuter avec ou sans réellement restituer quelque chose sur un écran. Pour Autowuzzlerj'utilise cette fonctionnalité pour réutiliser le code du monde physique à la fois pour le serveur et le client — avec plusieurs différences clés :

Physics world sur le serveur :

  • reçoit les entrées de l'utilisateur (événements de clavier pour diriger une voiture) via Colyseus et applique la force appropriée sur l'objet du jeu (la voiture de l'utilisateur) ;
  • effectue tous les calculs physiques pour tous les objets (joueurs et la balle), y compris la détection des collisions ;
  • communique l'état mis à jour pour chaque objet de jeu à Colyseus, qui à son tour le diffuse aux clients ;
  • est mis à jour toutes les 16,6 millisecondes (= 60 images par seconde), déclenché par notre serveur Colyseus.

Monde physique sur le client :

  • ne manipule pas directement les objets du jeu ;
  • reçoit l'état mis à jour pour chaque objet du jeu de Colyseus ;
  • applique les changements de position, vitesse et angle après réception de l'état mis à jour ;[1 9659031] envoie l'entrée utilisateur (événements de clavier pour diriger une voiture) à Colyseus ;
  • charge les sprites du jeu et utilise un moteur de rendu pour dessiner le monde physique sur un élément de canevas ;
  • ignore la détection de collision (en utilisant isSensor ]option pour les objets);
  • mises à jour à l'aide de requestAnimationFrame, idéalement à 60 ips.

Schéma montrant deux blocs principaux : Colyseus Server App et SvelteKit App. L'application Colyseus Server contient le bloc Autowuzzler Room, l'application SvelteKit contient le bloc client Colyseus. Les deux blocs principaux partagent un bloc nommé Physics World (Matter.js)

Unités logiques principales de l'architecture Autowuzzler : le Physics World est partagé entre le serveur Colyseus et l'application cliente SvelteKit. ( Grand aperçu)

Maintenant, avec toute la magie qui se produit sur le serveur, le client ne gère que l'entrée et dessine l'état qu'il reçoit du serveur à l'écran. À une exception près :

Interpolation sur le client

Comme nous réutilisons le même monde physique Matter.js sur le client, nous pouvons améliorer les performances expérimentées avec une astuce simple. Plutôt que de mettre à jour uniquement la position d'un objet de jeu, nous synchronisons également la vitesse de l'objet. De cette façon, l'objet continue de se déplacer sur sa trajectoire même si la prochaine mise à jour du serveur prend plus de temps que d'habitude. Ainsi, plutôt que de déplacer des objets par étapes discrètes de la position A à la position B, nous modifions leur position et les faisons se déplacer dans une certaine direction.

Lifecycle

La classe Autowuzzler Room est l'endroit où la logique concernant les différentes phases d'une salle Colyseus est traitée. Colyseus propose plusieurs méthodes de cycle de vie :

  • onCreate : lorsqu'une nouvelle salle est créée (généralement lorsque le premier client se connecte);
  • onAuth : comme un crochet d'autorisation pour autoriser ou refuser l'accès à la salle ;
  • onJoin : lorsqu'un client se connecte à la salle ;
  • onLeave : lorsqu'un client se déconnecte de la salle ;
  • onDispose : lorsque la salle est supprimée.

La salle Autowuzzler crée une nouvelle instance du monde de la physique (voir la section « Physics In A Colyseus App ») dès lors de sa création (onCreate) et ajoute un joueur au monde lorsqu'un client se connecte (onJoin). Il met ensuite à jour le monde de la physique 60 fois par seconde (toutes les 16,6 millisecondes) en utilisant la méthode setSimulationInterval (notre boucle de jeu principale) :

 // deltaTime est d'environ 16,6 millisecondes
this.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));

Les objets physiques sont indépendants des objets Colyseus, ce qui nous laisse deux permutations du même objet de jeu (comme la balle), c'est-à-dire un objet dans le monde de la physique et un objet Colyseus qui peut être synchronisé.

Dès que l'objet physique change, ses propriétés mises à jour doivent être réappliquées à l'objet Colyseus. Nous pouvons y parvenir en écoutant l'événement afterUpdate de Matter.js et en définissant les valeurs à partir de là :

Events.on(this.engine, "afterUpdate", () => {
 // applique la position x de l'objet boule physique à l'objet boule colyseus
 this.state.ball.x = this.physicsWorld.ball.position.x;
 // ... toutes les autres propriétés de la balle
 // boucle sur tous les joueurs de physique et applique leurs propriétés aux objets joueurs de colyseus
})

Il y a une copie supplémentaire des objets dont nous devons nous occuper : les objets du jeu dans le jeu en face-à-face.

Schéma montrant les trois versions d'un objet du jeu : Objets de schéma Colyseus, Objets de physique Matter.js, Client Objets de physique Matter.js. Matter.js met à jour la version Colyseus de l'objet, Colyseus se synchronise avec l'objet physique Client Matter.js.

Autowuzzler conserve trois copies de chaque objet physique, une version faisant autorité (objet Colyseus), une version dans la physique Matter.js monde et une version sur le client. ( Grand aperçu)

Application côté client

Maintenant que nous avons une application sur le serveur qui gère la synchronisation de l'état du jeu pour plusieurs pièces ainsi que les calculs physiques, concentrons-nous sur la construction du site Web et de l'interface de jeu réelle . L'interface Autowuzzler a les responsabilités suivantes :

  • permet aux utilisateurs de créer et de partager des codes PIN de jeu pour accéder à des salles individuelles ;
  • envoie les codes PIN de jeu créés à une base de données Supabase pour la persistance ;
  • fournit un page facultative « Rejoindre un jeu » pour que les joueurs saisissent le code PIN du jeu ;
  • valide les codes PIN du jeu lorsqu'un joueur rejoint un jeu ;
  • héberge et rend le jeu réel sur une URL partageable (c'est-à-dire unique) ;
  • se connecte au serveur Colyseus et gérer les mises à jour d'état ;
  • fournit une page de renvoi (« marketing »).

Pour la mise en œuvre de ces tâches, j'ai choisi SvelteKit plutôt que Next.js pour les raisons suivantes :

Pourquoi SvelteKit ?

Je voulais développer une autre application en utilisant Svelte depuis que j'ai construit neolightsout. Lorsque SvelteKit (le framework d'application officiel pour Svelte) est entré en version bêta publique, j'ai décidé de construire Autowuzzler avec lui et d'accepter tous les maux de tête liés à l'utilisation d'une nouvelle version bêta – la joie d'utiliser Svelte le compense clairement. .

Ces fonctionnalités clés m'ont fait choisir SvelteKit plutôt que Next.js pour l'implémentation réelle du frontend du jeu :

  • Svelte est un framework d'interface utilisateur et un compilateur et est donc livré code minimal sans environnement d'exécution client ;
  • Svelte dispose d'un langage de modèles expressif et d'un système de composants (préférence personnelle) ;
  • Svelte inclut des magasins globaux, des transitions et des animations prêtes à l'emploi, ce qui signifie : aucune fatigue de décision en choisissant un état global une boîte à outils de gestion et une bibliothèque d'animations ;
  • Svelte prend en charge le CSS étendu dans des composants à fichier unique ;
  • SvelteKit prend en charge le SSR, un routage simple mais flexible basé sur des fichiers et des routes côté serveur pour la création d'une API ;
  • SvelteKi t permet à chaque page d'exécuter du code sur le serveur, par ex. pour récupérer les données utilisées pour afficher la page ;
  • Mise en page partagée entre les itinéraires ;
  • SvelteKit peut être exécuté dans un environnement sans serveur.

Création et stockage des codes PIN du jeu

Avant qu'un utilisateur puisse commencer à jouer au jeu , ils doivent d'abord créer un code PIN de jeu. En partageant le code PIN avec d'autres, ils peuvent tous accéder à la même salle de jeu.

 Capture d'écran du démarrage d'une nouvelle section de jeu du site Web Autowuzzler montrant le code PIN du jeu 751428 et les options pour copier et partager le code PIN et l'URL du jeu.[19659009]Démarrez un nouveau jeu en copiant le code PIN de jeu généré ou partagez le lien direct vers la salle de jeux. (<a href= Grand aperçu)

Il s'agit d'un excellent cas d'utilisation pour les endpoints côté serveur de Sveltes en conjonction avec la fonction Sveltes onMount : le point d'extrémité /api/createcode génère un code PIN de jeu , le stocke dans une base de données Supabase.io et génère le code PIN du jeu en tant que réponse . Cette réponse est récupérée dès que le composant de page de la page « créer » est monté :

Diagramme montrant trois sections : Créer une page, un point de terminaison de création de code et Supabase.io. La page de création récupère le point de terminaison dans sa fonction onMount, le point de terminaison génère un code PIN de jeu, le stocke dans Supabase.io et répond avec le code PIN de jeu. La page Créer affiche ensuite le code PIN du jeu.

Les codes PIN du jeu sont créés dans le point de terminaison, stockés dans une base de données Supabase.io et affichés sur la page « Créer ». ( Grand aperçu)

Stockage des codes PIN de jeu avec Supabase.io

Supabase.io est une alternative open source à Firebase. Supabase permet de créer très facilement une base de données PostgreSQL et d'y accéder soit via l'une de ses bibliothèques clientes, soit via REST.

Pour le client JavaScript, nous importons la fonction createClient et l'exécutons à l'aide des paramètres supabase_url et supabase_key que nous avons reçues lors de la création de la base de données. Pour stocker le code PIN du jeu qui est créé à chaque appel vers le point de terminaison createcodeil suffit d'exécuter cette simple requête insert :

 importer { createClient } depuis '@supabase/supabase-js'

const database = createClient(
 import.meta.env.VITE_SUPABASE_URL,
 import.meta.env.VITE_SUPABASE_KEY
);

const { données, erreur } = attend la base de données
 .from("jeux")
 .insert([{ code: 123456 }]);

Remarque : Les supabase_url et supabase_key sont stockées dans un fichier .env. En raison de Vite – l'outil de construction au cœur de SvelteKit – il est nécessaire de préfixer les variables d'environnement avec VITE_ pour les rendre accessibles dans SvelteKit.

Accéder au jeu

Je voulais rejoindre un Autowuzzler jeu aussi simple que de suivre un lien. Par conséquent, chaque salle de jeu devait avoir sa propre URL basée sur le code PIN de jeu précédemment créé par ex. https://autowuzzler.com/play/12345.

Dans SvelteKit, les pages avec des paramètres de route dynamiques sont créées en mettant les parties dynamiques de la route entre crochets lors du nom du fichier de page : client/src/routes/play/[gamePIN].svelte. La valeur du paramètre gamePIN deviendra alors disponible dans le composant de page (voir la SvelteKit docs pour plus de détails). Dans la route playnous devons nous connecter au serveur Colyseus, instancier le monde physique pour le rendu à l'écran, gérer les mises à jour des objets du jeu, écouter la saisie au clavier et afficher d'autres interfaces utilisateur comme le score, etc. on.

Connexion à Colyseus et mise à jour de l'état

La bibliothèque cliente Colyseus nous permet de connecter un client à un serveur Colyseus. Tout d'abord, créons un nouveau Colyseus.Client en le pointant vers le serveur Colyseus (ws://localhost:2567en développement). Rejoignez ensuite la salle avec le nom que nous avons choisi précédemment (autowuzzler) et le gamePIN à partir du paramètre route. Le paramètre gamePIN s'assure que l'utilisateur rejoint la bonne instance de salle (voir "match-making" ci-dessus).

let client = new Colyseus.Client("ws:// localhost:2567");
this.room = wait client.joinOrCreate("autowuzzler", { gamePIN });

Étant donné que SvelteKit affiche initialement les pages sur le serveur, nous devons nous assurer que ce code ne s'exécute sur le client qu'une fois le chargement de la page terminé. Encore une fois, nous utilisons la fonction de cycle de vie onMount pour ce cas d'utilisation. (Si vous connaissez React, onMount est similaire au hook useEffect avec un tableau de dépendances vide.)

onMount(async () => {
  let client = new Colyseus.Client("ws://localhost:2567");
  this.room = wait client.joinOrCreate("autowuzzler", { gamePIN });
})

Maintenant que nous sommes connectés au serveur de jeu Colyseus, nous pouvons commencer à écouter les modifications apportées à nos objets de jeu.

Voici un exemple de comment écouter un joueur rejoindre la salle ( onAdd) et recevoir des mises à jour d'état consécutives pour ce lecteur :

this.room.state.players.onAdd = (player, key) => {
  console.log(`Le joueur a été ajouté avec sessionId : ${key}`);

  // ajoute une entité de joueur au monde du jeu
  this.world.createPlayer(clé, player.teamNumber);

  // écoute les modifications apportées à ce lecteur
  player.onChange = (changements) => {
   changes.forEach(({ champ, valeur }) => {
     this.world.updatePlayer(clé, champ, valeur); // voir ci-dessous
   });
 } ;
} ;

Dans la méthode updatePlayer du monde de la physique, nous mettons à jour les propriétés une par une car onChange de Colyseus fournit un ensemble de toutes les propriétés modifiées.

Note  : Cette fonction ne s'exécute que sur la version cliente du monde de la physique, car les objets du jeu ne sont manipulés qu'indirectement via le serveur Colyseus.

updatePlayer(sessionId, field, value) {
 // récupère l'objet physique du joueur par son sessionId
 let player = this.world.players.get(sessionId);
 // sortie si introuvable
 si (!player) revient ;
 // applique les modifications aux propriétés
 commutateur (champ) {
   cas "angle":
     Body.setAngle(player, value);
     Pause;
   cas "x":
     Body.setPosition(player, { x : valeur, y : player.position.y });
     Pause;
   cas "y":
     Body.setPosition(player, { x: player.position.x, y: value });
     Pause;
   // définit la vitesseX, la vitesseY, la vitesse angulaire ...
 }
}

La même procédure s'applique aux autres objets du jeu (ballon et équipes) : écoutez leurs modifications et appliquez les valeurs modifiées au monde physique du client.

Jusqu'à présent, aucun objet ne bouge car nous devons encore ]écouter la saisie au clavier et l'envoyer au serveur. Au lieu d'envoyer directement des événements sur chaque événement keydownnous maintenons une carte des touches actuellement enfoncées et envoyons les événements au serveur Colyseus dans une boucle de 50 ms. De cette façon, nous pouvons prendre en charge l'appui sur plusieurs touches en même temps et atténuer la pause qui se produit après le premier et les événements consécutifs keydown lorsque la touche reste enfoncée :

let keys = {} ;
const keyDown = e => {
 clés[e.key] = vrai;
} ;
const keyUp = e => {
 clés[e.key] = faux;
} ;
document.addEventListener('keydown', keyDown);
document.addEventListener('keyup', keyUp);

let loop = () => {
 si (clés["ArrowLeft"]) {
   this.room.send("move", { direction: "left" });
 }
 else if (clés["ArrowRight"]) {
   this.room.send("move", { direction: "right" });
 }
 si (clés["ArrowUp"]) {
   this.room.send("move", { direction: "up" });
 }
 else if (clés["ArrowDown"]) {
   this.room.send("move", { direction: "down" });
 }
 // prochaine itération
 requestAnimationFrame(() => {
  setTimeout(boucle, 50);
 });
}
// démarrer la boucle
setTimeout(boucle, 50);

Maintenant, le cycle est terminé : écoutez les frappes, envoyez les commandes correspondantes au serveur Colyseus pour manipuler le monde de la physique sur le serveur. Le serveur Colyseus applique ensuite les nouvelles propriétés physiques à tous les objets du jeu et propage les données au client pour mettre à jour l'instance du jeu destinée à l'utilisateur.

Minor Nuisances

Rétrospectivement, deux choses de la catégorie personne-ne-m'en-dit-mais-quelqu'un-aurait dû venir à l'esprit :

  • Une bonne compréhension du fonctionnement des moteurs physiques est bénéfique. J'ai passé un temps considérable à peaufiner les propriétés et les contraintes de la physique. Même si j'avais déjà construit un petit jeu avec Phaser.js et Matter.js, il y avait beaucoup d'essais et d'erreurs pour faire bouger les objets comme je les imaginais.
  • Le temps réel, c'est hard – en particulier dans les jeux basés sur la physique. Des retards mineurs aggravent considérablement l'expérience, et bien que la synchronisation de l'état entre les clients avec Colyseus fonctionne très bien, elle ne peut pas supprimer les retards de calcul et de transmission. beta-oven, il y avait quelques pièges et mises en garde que je voudrais souligner :

    • Il a fallu un certain temps pour comprendre que les variables d'environnement doivent être préfixées par VITE_ afin de les utiliser dans SvelteKit. Ceci est maintenant correctement documenté dans la FAQ.
    • Pour utiliser Supabase, j'ai dû ajouter Supabase à à la fois les dépendances et devDependencies listes de package.json. Je pense que ce n'est plus le cas.
    • La fonction SvelteKits load s'exécute à la fois sur le serveur et sur le client !
    • Pour activer le remplacement complet du module à chaud (y compris la préservation de l'état) , vous devez ajouter manuellement une ligne de commentaire dans les composants de votre page. Voir FAQ pour plus de détails.

    De nombreux autres frameworks auraient également été parfaits, mais je ne regrette pas d'avoir choisi SvelteKit pour ce projet. Cela m'a permis de travailler sur l'application client de manière très efficace – principalement parce que Svelte lui-même est très expressif et saute une grande partie du code passe-partout, mais aussi parce que Svelte contient des éléments tels que des animations, des transitions, des CSS étendus et des magasins globaux. SvelteKit a fourni tous les blocs de construction dont j'avais besoin (SSR, routage, routes de serveur) et bien qu'encore en version bêta, il s'est avéré très stable et rapide.

    Déploiement et hébergement

    Initialement, j'ai hébergé le Colyseus (Node) sur une instance Heroku et a perdu beaucoup de temps à faire fonctionner WebSockets et CORS. Il s'avère que les performances d'un petit dyno Heroku (gratuit) ne sont pas suffisantes pour un cas d'utilisation en temps réel. J'ai ensuite migré l'application Colyseus vers un petit serveur à Linode. L'application côté client est déployée et hébergée sur Netlify via SvelteKits adapter-netlify. Pas de surprises ici : Netlify a juste très bien fonctionné !

    Conclusion

    Commencer avec un prototype très simple pour valider l'idée m'a beaucoup aidé à déterminer si le projet vaut la peine d'être suivi et où se situent les défis techniques du jeu. Dans la mise en œuvre finale, Colyseus s'est occupé de tout le gros du travail de synchronisation de l'état en temps réel sur plusieurs clients, répartis dans plusieurs salles. It’s impressive how quickly a real-time multi-user application can be built with Colyseus — once you figure out how to properly describe the schema. Colyseus’ built-in monitoring panel helps in troubleshooting any synchronizing issues.

    What complicated this setup was the physics layer of the game because it introduced an additional copy of each physics-related game object that needed to be maintained. Storing game PINs in Supabase.io from the SvelteKit app was very straightforward. In hindsight, I could have just used an SQLite database to store the game PINs, but trying out new things is half of the fun when building side projects.

    Finally, using SvelteKit for building out the frontend of the game allowed me to move quickly — and with the occasional grin of joy on my face.

    Now, go ahead and invite your friends to a round of Autowuzzler!

    Further Reading on Smashing Magazine

    Smashing Editorial" width="35" height="46" loading="lazy" decoding="async(vf, il)




Source link