Comment créer un jeu multi-utilisateurs en temps réel à partir de zéro
Publié: 2022-03-10Alors 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 éloigné, mais il était clair que simplement reconstruire les règles du baby-foot sur un écran ne serait pas très amusant.
Ce qui est amusant, c'est de taper dans un ballon en utilisant des petites voitures - une réalisation faite 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 allait devenir Autowuzzler .
L'idée est simple : les joueurs dirigent des voitures miniatures virtuelles dans une arène de haut en bas qui ressemble à un 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 différencier Autowuzzler : je voulais reconstruire une partie de l'apparence et de la sensation de jouer sur un baby-foot physique, et je voulais m'assurer que c'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'Autowuzzler , les outils et les frameworks que j'ai choisis, et je partagerai quelques détails de mise en œuvre et les leçons que j'ai apprises.
Premier prototype fonctionnel (terrible)
Le premier prototype a été construit à l'aide du moteur de jeu open source Phaser.js, principalement pour le moteur physique inclus et parce que j'en avais déjà une certaine expérience. L'étape du jeu était intégrée dans une application Next.js, encore 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 prendre en charge plusieurs joueurs en temps réel , j'ai utilisé Express en tant que courtier WebSockets. C'est là que ça devient délicat, cependant.
Étant donné que les calculs physiques étaient effectués sur le client dans le jeu Phaser, j'ai choisi une logique simple, mais évidemment 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 alors les modifications aux objets du jeu.
Cela a conduit à la situation où le premier joueur a pu voir la physique se produire en temps réel (cela se passe localement dans son navigateur, après tout), tandis 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 - bien pire.
Si cela vous semble être 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, vider le prototype
Aussi imparfaite que soit la mise en œuvre, elle était suffisamment jouable pour inviter des amis à un premier essai routier. Les commentaires ont été très positifs , la principale préoccupation étant - sans surprise - les performances en temps réel. D'autres problèmes inhérents incluaient la situation où le premier joueur (rappelez-vous, celui 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 n'importe qui rejoindrait le même jeu. J'étais également un peu préoccupé par la taille du bundle introduite par la bibliothèque Phaser.js.
Il était temps de jeter le prototype et de commencer avec une nouvelle configuration et un objectif clair.
Configuration du projet
De toute évidence, l'approche « le premier client règle tout » devait être remplacée par une solution dans laquelle l'état du jeu vit sur le serveur . Dans mes recherches, je suis tombé sur Colyseus, qui semblait être l'outil parfait pour le travail.
Pour les autres blocs de construction principaux du jeu, j'ai choisi :
- Matter.js en tant que moteur physique au lieu de Phaser.js car il s'exécute dans Node et Autowuzzler ne nécessite pas de cadre de jeu complet.
- SvelteKit en tant que framework d'application au lieu de Next.js, car il vient de passer 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.
Examinons ces blocs de construction plus en détail.
État de jeu synchronisé et centralisé avec Colyseus
Colyseus est un framework de jeu multijoueur basé sur Node.js et Express. À la base, il fournit :
- Synchronisation de l'état entre les clients d'une manière faisant autorité ;
- Communication efficace en temps réel à 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 ;
- Crochets de cycle de vie, par exemple, la salle est créée, les jonctions d'utilisateurs, les départs d'utilisateurs, etc.
- 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 : La documentation Colyseus facilite la prise en main d'un serveur barebones Colyseus en fournissant un script d' 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 instance de salle unique et de tous ses objets de jeu. Dans le cas d' Autowuzzler , il s'agit d'une session de jeu avec :
- deux equipes,
- un nombre fini de joueurs,
- une balle.
Un schéma doit être défini pour toutes les propriétés des objets du jeu qui doivent être synchronisés 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 { constructor() { super(); this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; } } defineTypes(Ball, { x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number" });
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 : x
, y
, angle
, velocityX,
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és peuvent être soit des types primitifs :
-
string
-
boolean
-
number
(ainsi que des types entiers et flottants plus efficaces)
ou 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 a cinq propriétés de type number
: ses coordonnées ( x
, y
), son angle
courant et le vecteur velocityX
( VelocityX , velocityY
).
Le schéma 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 extends Schema { constructor(teamNumber) { super(); this.name = ""; this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; this.teamNumber = teamNumber; } } defineTypes(Player, { name: "string", x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number", angularVelocity: "number", teamNumber: "number", });
Enfin, le schéma de la Room
Autowuzzler connecte les classes précédemment définies : une instance de salle a plusieurs équipes (stockées dans un ArraySchema). Il contient également une seule balle, nous créons donc une nouvelle instance 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.
Configuration multi-pièces ("Match-Making")
N'importe qui peut rejoindre un jeu Autowuzzler s'il possède 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 et abandonne la salle lorsque le dernier joueur la quitte.
Le processus d' affectation des joueurs à la salle de jeu souhaitée est appelé "match-making". Colyseus le rend très simple à mettre en place en utilisant la méthode filterBy
lors de la définition d'une nouvelle pièce :
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.
Physique dans une application Colyseus
Colyseus fournit de nombreux éléments prêts à l'emploi pour être rapidement opérationnel avec un serveur de jeu faisant autorité, 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é Matter.js de Phaser.js peut s'exécuter sur Node.js.
Avec Matter.js, vous définissez un monde 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, y compris la masse, les collisions, le mouvement avec frottement, 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 la vitesse à laquelle les voitures se déplacent, le rebond du ballon, l'emplacement des buts et ce qui se passe si quelqu'un tire un but.
let ball = Bodies.circle( ballInitialXPosition, ballInitialYPosition, radius, { render: { sprite: { texture: '/assets/ball.png', } }, friction: 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 rendu réel sur un écran. Pour Autowuzzler , j'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 :
Monde physique sur le serveur :
- reçoit les entrées de l'utilisateur (événements du 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 ballon), y compris la détection des collisions ;
- communique l'état mis à jour de 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 de jeu de Colyseus ;
- applique les changements de position, de vitesse et d'angle après avoir reçu l'état mis à jour ;
- envoie les entrées de l'utilisateur (événements du 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 ;
- saute la détection de collision (en utilisant l'option
isSensor
pour les objets); - mises à jour en utilisant requestAnimationFrame, idéalement à 60 fps.
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 vers l'écran. A une exception près :
Interpolation sur le client
Puisque nous réutilisons le même monde physique Matter.js sur le client, nous pouvons améliorer les performances expérimentées avec une simple astuce. 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.
Cycle de la vie
La classe Autowuzzler Room
est l'endroit où la logique concernée par les différentes phases d'une salle Colyseus est gérée. Colyseus fournit 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 crochet d'autorisation pour autoriser ou refuser l'entrée dans la salle ; -
onJoin
: lorsqu'un client se connecte à la salle ; -
onLeave
: lorsqu'un client se déconnecte de la salle ; -
onDispose
: lorsque la pièce est supprimée.
La salle Autowuzzler crée une nouvelle instance du monde de la physique (voir la section "La physique dans une application Colyseus") dès sa création ( onCreate
) et ajoute un joueur au monde lorsqu'un client se connecte ( onJoin
). Il met ensuite à jour le monde physique 60 fois par seconde (toutes les 16,6 millisecondes) en utilisant la méthode setSimulationInterval
(notre boucle de jeu principale) :
// deltaTime is roughly 16.6 milliseconds this.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));
Les objets physiques sont indépendants des objets Colyseus, ce qui nous laisse avec deux permutations du même objet de jeu (comme la balle), c'est-à-dire un objet dans le monde physique et un objet Colyseus qui peut être synchronisé.
Dès que l'objet physique change, ses propriétés mises à jour doivent être appliquées à l'objet Colyseus. Nous pouvons y parvenir en écoutant l'événement afterUpdate de afterUpdate
et en définissant les valeurs à partir de là :
Events.on(this.engine, "afterUpdate", () => { // apply the x position of the physics ball object back to the colyseus ball object this.state.ball.x = this.physicsWorld.ball.position.x; // ... all other ball properties // loop over all physics players and apply their properties back to colyseus players objects })
Il y a une autre copie des objets dont nous devons nous occuper : les objets du jeu dans le jeu en contact avec l'utilisateur .
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 salles ainsi que les calculs physiques, concentrons-nous sur la construction du site Web et de l'interface de jeu proprement dite . 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 une 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ère les mises à jour d'état ;
- fournit une page de destination ("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 cadre d'application officiel de Svelte) est entré en version bêta publique, j'ai décidé de construire Autowuzzler avec 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 de l'interface du jeu :
- Svelte est un framework d'interface utilisateur et un compilateur et fournit donc un code minimal sans runtime client ;
- Svelte a un langage de modèles expressif et 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 : pas de fatigue décisionnelle en choisissant une boîte à outils de gestion d'état globale et une bibliothèque d'animations ;
- Svelte prend en charge les CSS délimités dans les composants à fichier unique ;
- SvelteKit prend en charge SSR, un routage simple mais flexible basé sur des fichiers et des routes côté serveur pour créer une API ;
- SvelteKit permet à chaque page d'exécuter du code sur le serveur, par exemple pour récupérer les données utilisées pour afficher la page ;
- Dispositions partagées entre les itinéraires ;
- SvelteKit peut être exécuté dans un environnement sans serveur.
Créer et stocker des codes PIN de jeu
Avant qu'un utilisateur puisse commencer à jouer au jeu, il doit 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 jeux.
Il s'agit d'un excellent cas d'utilisation pour les points de terminaison côté serveur SvelteKits en conjonction avec la fonction Sveltes onMount : le point de terminaison /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 réponse . Cette réponse est récupérée dès que le composant page de la page "créer" est monté :
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 en utilisant les paramètres supabase_url
et supabase_key
que nous avons reçus lors de la création de la base de données. Pour stocker le code PIN du jeu créé à chaque appel au point de terminaison createcode
, il nous suffit d'exécuter cette simple requête d' insert
:
import { createClient } from '@supabase/supabase-js' const database = createClient( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_KEY ); const { data, error } = await database .from("games") .insert([{ code: 123456 }]);
Remarque : La 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 rendre la participation à un jeu Autowuzzler 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 du jeu précédemment créé , par exemple 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 nommage du fichier de page : client/src/routes/play/[gamePIN].svelte
. La valeur du paramètre gamePIN
deviendra alors disponible dans le composant de la page (voir la documentation SvelteKit pour plus de détails). Dans le parcours de play
, nous devons nous connecter au serveur Colyseus, instancier le monde physique pour le rendre à l'écran, gérer les mises à jour des objets du jeu, écouter les entrées au clavier et afficher d'autres interfaces utilisateur comme le score, etc.
Connexion à Colyseus et mise à jour de l'état
La bibliothèque client Colyseus nous permet de connecter un client à un serveur Colyseus. Commençons par créer un nouveau Colyseus.Client
en le faisant pointer vers le serveur Colyseus ( ws://localhost:2567
en développement). Rejoignez ensuite la salle avec le nom que nous avons choisi plus tôt ( autowuzzler
) et le gamePIN
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 = await client.joinOrCreate("autowuzzler", { gamePIN });
Étant donné que SvelteKit rend initialement les pages sur le serveur, nous devons nous assurer que ce code ne s'exécute sur le client qu'une fois la page chargée. 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 crochet useEffect
avec un tableau de dépendances vide.)
onMount(async () => { let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN }); })
Maintenant que nous sommes connectés au serveur de jeu Colyseus, nous pouvons commencer à écouter toute modification apportée à nos objets de jeu.
Voici un exemple de la façon d'écouter un joueur rejoignant la salle ( onAdd
) et de recevoir des mises à jour consécutives de l'état de ce joueur :
this.room.state.players.onAdd = (player, key) => { console.log(`Player has been added with sessionId: ${key}`); // add player entity to the game world this.world.createPlayer(key, player.teamNumber); // listen for changes to this player player.onChange = (changes) => { changes.forEach(({ field, value }) => { this.world.updatePlayer(key, field, value); // see below }); }; };
Dans la méthode updatePlayer
du monde de la physique, nous mettons à jour les propriétés une par une car onChange de onChange
fournit un ensemble de toutes les propriétés modifiées.
Note : Cette fonction ne fonctionne que sur la version client du monde physique, car les objets du jeu ne sont manipulés qu'indirectement via le serveur Colyseus.
updatePlayer(sessionId, field, value) { // get the player physics object by its sessionId let player = this.world.players.get(sessionId); // exit if not found if (!player) return; // apply changes to the properties switch (field) { case "angle": Body.setAngle(player, value); break; case "x": Body.setPosition(player, { x: value, y: player.position.y }); break; case "y": Body.setPosition(player, { x: player.position.x, y: value }); break; // set velocityX, velocityY, angularVelocity ... } }
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 toujours écouter la saisie au clavier et l'envoyer au serveur . Au lieu d'envoyer directement des événements sur chaque événement de keydown
, nous maintenons une carte des touches actuellement enfoncées et envoyons des é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 événement et les suivants lorsque la touche reste keydown
:
let keys = {}; const keyDown = e => { keys[e.key] = true; }; const keyUp = e => { keys[e.key] = false; }; document.addEventListener('keydown', keyDown); document.addEventListener('keyup', keyUp); let loop = () => { if (keys["ArrowLeft"]) { this.room.send("move", { direction: "left" }); } else if (keys["ArrowRight"]) { this.room.send("move", { direction: "right" }); } if (keys["ArrowUp"]) { this.room.send("move", { direction: "up" }); } else if (keys["ArrowDown"]) { this.room.send("move", { direction: "down" }); } // next iteration requestAnimationFrame(() => { setTimeout(loop, 50); }); } // start loop setTimeout(loop, 50);
Maintenant, le cycle est terminé : écoutez les frappes, envoyez les commandes correspondantes au serveur Colyseus pour manipuler le monde 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 vers le client pour mettre à jour l'instance du jeu destinée à l'utilisateur.
Nuisances mineures
Rétrospectivement, deux choses de la catégorie personne ne m'a dit mais quelqu'un aurait dû me 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 physiques. Même si j'ai déjà créé un petit jeu avec Phaser.js et Matter.js, il y a eu beaucoup d'essais et d'erreurs pour faire bouger les objets comme je les imaginais.
- Le temps réel est difficile , 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.
Pièges et mises en garde avec SvelteKit
Depuis que j'ai utilisé SvelteKit quand il sortait tout juste du four bêta, 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 aux listes de
dependencies
et dedevDependencies
de package.json. Je crois que ce n'est plus le cas. - La fonction de
load
SvelteKits 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
<!-- @hmr:keep-all -->
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 cliente 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 a des choses comme des animations, des transitions, des CSS étendus et des magasins mondiaux intégrés. SvelteKit a fourni tous les éléments de base dont j'avais besoin (SSR, routage, routes de serveur) et bien qu'encore en version bêta, il était très stable et rapide.
Déploiement et hébergement
Au départ, j'hébergeais le serveur Colyseus (Node) sur une instance Heroku et j'ai 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 chez Linode. L'application côté client est déployée et hébergée sur Netlify via l'adaptateur SvelteKits-netlify. Pas de surprise ici : Netlify a très bien fonctionné !
Conclusion
Commencer avec un prototype vraiment 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 pièces. Il est impressionnant de voir à quelle vitesse une application multi-utilisateurs en temps réel peut être construite avec Colyseus - une fois que vous avez compris comment décrire correctement le schéma. Le panneau de surveillance intégré de Colyseus aide à résoudre les problèmes de synchronisation.
Ce qui compliquait cette configuration était la couche physique du jeu car elle introduisait une copie supplémentaire de chaque objet de jeu lié à la physique qui devait être maintenu. Le stockage des codes PIN de jeu dans Supabase.io à partir de l'application SvelteKit était très simple. Avec le recul, j'aurais pu simplement utiliser une base de données SQLite pour stocker les codes PIN du jeu, mais essayer de nouvelles choses est la moitié du plaisir lors de la création de projets parallèles.
Enfin, l'utilisation de SvelteKit pour créer l'interface du jeu m'a permis d'avancer rapidement - et avec un sourire de joie occasionnel sur mon visage.
Maintenant, allez-y et invitez vos amis à une partie d'Autowuzzler !
Lectures complémentaires sur Smashing Magazine
- "Commencez avec React en créant un jeu Whac-A-Mole", Jhey Tompkins
- "Comment créer un jeu de réalité virtuelle multijoueur en temps réel", Alvin Wan
- "Écrire un moteur d'aventure textuelle multijoueur dans Node.js", Fernando Doglio
- "L'avenir de la conception de sites Web mobiles : conception de jeux vidéo et narration", Suzanne Scacca
- "Comment créer un jeu de course sans fin en réalité virtuelle", Alvin Wan