Come creare un gioco multiutente in tempo reale da zero
Pubblicato: 2022-03-10Con il persistere della pandemia, la squadra improvvisamente remota con cui lavoro è diventata sempre più priva di biliardino. Ho pensato a come giocare a biliardino in un ambiente remoto, ma era chiaro che ricostruire semplicemente le regole del biliardino su uno schermo non sarebbe stato molto divertente.
Ciò che è divertente è calciare un pallone usando le macchinine, una realizzazione realizzata mentre stavo giocando con mio figlio di 2 anni. La stessa notte ho deciso di costruire il primo prototipo per un gioco che sarebbe diventato Autowuzzler .
L'idea è semplice : i giocatori guidano macchinine virtuali in un'arena dall'alto che ricorda un biliardino. Vince la prima squadra che segna 10 gol.
Naturalmente, l'idea di usare le auto per giocare a calcio non è unica, ma due idee principali dovrebbero distinguere Autowuzzler : volevo ricostruire parte dell'aspetto e delle sensazioni di giocare su un biliardino fisico e volevo assicurarmi che lo fosse il più semplice possibile per invitare amici o compagni di squadra a un veloce casual game.
In questo articolo, descriverò il processo alla base della creazione di Autowuzzler , quali strumenti e framework ho scelto e condividerò alcuni dettagli di implementazione e lezioni che ho imparato.
Primo prototipo funzionante (terribile).
Il primo prototipo è stato realizzato utilizzando il motore di gioco open source Phaser.js, principalmente per il motore fisico incluso e perché avevo già una certa esperienza con esso. La fase di gioco era incorporata in un'applicazione Next.js, sempre perché avevo già una solida conoscenza di Next.js e volevo concentrarmi principalmente sul gioco.
Poiché il gioco deve supportare più giocatori in tempo reale , ho utilizzato Express come broker WebSockets. Qui è dove diventa complicato, però.
Poiché i calcoli di fisica sono stati eseguiti sul client nel gioco Phaser, ho scelto una logica semplice, ma ovviamente imperfetta: il primo client connesso aveva il dubbio privilegio di eseguire i calcoli di fisica per tutti gli oggetti di gioco, inviando i risultati al server espresso, che a sua volta ha trasmesso le posizioni, gli angoli e le forze aggiornate ai clienti dell'altro giocatore. Gli altri client applicherebbero quindi le modifiche agli oggetti di gioco.
Ciò ha portato alla situazione in cui il primo giocatore ha potuto vedere la fisica in tempo reale (dopotutto sta accadendo localmente nel proprio browser), mentre tutti gli altri giocatori erano in ritardo di almeno 30 millisecondi (la velocità di trasmissione che ho scelto ), o — se la connessione di rete del primo giocatore era lenta — considerevolmente peggio.
Se questo ti suona come un'architettura scadente, hai assolutamente ragione. Tuttavia, ho accettato questo fatto a favore di ottenere rapidamente qualcosa di giocabile per capire se il gioco è davvero divertente da giocare.
Convalida l'idea, scarica il prototipo
Per quanto imperfetta fosse l'implementazione, era sufficientemente giocabile da invitare gli amici per un primo giro di prova. Il feedback è stato molto positivo , con la principale preoccupazione, non sorprendentemente, la performance in tempo reale. Altri problemi intrinseci includevano la situazione in cui il primo giocatore (ricordate, il responsabile di tutto ) ha lasciato il gioco: chi dovrebbe subentrare? A questo punto c'era solo una sala giochi, quindi chiunque si sarebbe unito allo stesso gioco. Ero anche un po' preoccupato per la dimensione del pacchetto introdotta dalla libreria Phaser.js.
Era ora di scaricare il prototipo e iniziare con una nuova configurazione e un obiettivo chiaro.
Configurazione del progetto
Chiaramente, l'approccio "il primo client regola tutto" doveva essere sostituito con una soluzione in cui lo stato del gioco risieda sul server . Nella mia ricerca, mi sono imbattuto in Colyseus, che sembrava lo strumento perfetto per il lavoro.
Per gli altri elementi costitutivi principali del gioco ho scelto:
- Matter.js come motore fisico invece di Phaser.js perché viene eseguito in Node e Autowuzzler non richiede un framework di gioco completo.
- SvelteKit come framework applicativo invece di Next.js, perché in quel momento è appena entrato in versione beta pubblica. (Inoltre: adoro lavorare con Svelte.)
- Supabase.io per memorizzare i PIN di gioco creati dall'utente.
Diamo un'occhiata a questi elementi costitutivi in modo più dettagliato.
Stato di gioco sincronizzato e centralizzato con Colyseus
Colyseus è un framework di gioco multiplayer basato su Node.js ed Express. Al suo interno, fornisce:
- Sincronizzare lo stato tra i clienti in modo autorevole;
- Comunicazione efficiente in tempo reale tramite WebSocket inviando solo i dati modificati;
- Configurazioni multi-stanza;
- Librerie client per JavaScript, Unity, Defold Engine, Haxe, Cocos Creator, Construct3;
- Hook del ciclo di vita, ad esempio la creazione di una stanza, l'accesso dell'utente, l'abbandono dell'utente e altro ancora;
- Invio di messaggi, sia come messaggi broadcast a tutti gli utenti nella stanza, sia a un singolo utente;
- Un pannello di monitoraggio integrato e uno strumento di test del carico.
Nota : i documenti Colyseus rendono facile iniziare con un server Colyseus barebone fornendo uno script npm init
e un repository di esempi.
Creazione di uno schema
L'entità principale di un'app Colyseus è la sala giochi, che contiene lo stato di una singola istanza della stanza e di tutti i suoi oggetti di gioco. Nel caso di Autowuzzler , si tratta di una sessione di gioco con:
- due squadre,
- un numero limitato di giocatori,
- una palla.
È necessario definire uno schema per tutte le proprietà degli oggetti di gioco che devono essere sincronizzati tra i client . Ad esempio, vogliamo che la palla si sincronizzi, quindi dobbiamo creare uno schema per la palla:
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" });
Nell'esempio sopra, viene creata una nuova classe che estende la classe dello schema fornita da Colyseus; nel costruttore, tutte le proprietà ricevono un valore iniziale. La posizione e il movimento della palla sono descritti utilizzando le cinque proprietà: x
, y
, angle
, velocityX,
velocityY
. Inoltre, è necessario specificare i tipi di ciascuna proprietà . Questo esempio usa la sintassi JavaScript, ma puoi anche usare la sintassi TypeScript leggermente più compatta.
I tipi di proprietà possono essere tipi primitivi:
-
string
-
boolean
-
number
(oltre a tipi interi e float più efficienti)
o tipi complessi:
-
ArraySchema
(simile a Array in JavaScript) -
MapSchema
(simile a Map in JavaScript) -
SetSchema
(simile a Set in JavaScript) -
CollectionSchema
(simile a ArraySchema, ma senza controllo sugli indici)
La classe Ball
sopra ha cinque proprietà di tipo number
: le sue coordinate ( x
, y
), il suo angle
corrente e il vettore di velocità ( velocityX
, velocityY
).
Lo schema per i giocatori è simile, ma include alcune proprietà in più per memorizzare il nome del giocatore e il numero della squadra, che devono essere forniti durante la creazione di un'istanza 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", });
Infine, lo schema per l' Autowuzzler Room
collega le classi precedentemente definite: un'istanza di room ha più team (memorizzati in un ArraySchema). Contiene anche una singola sfera, quindi creiamo una nuova istanza Ball nel costruttore di RoomSchema. I giocatori vengono archiviati in un MapSchema per un rapido recupero utilizzando i loro ID.
Configurazione multistanza ("Match-Making")
Chiunque può partecipare a una partita di Autowuzzler se dispone di un PIN di gioco valido. Il nostro server Colyseus crea una nuova istanza Room per ogni sessione di gioco non appena il primo giocatore si unisce e scarta la stanza quando l'ultimo giocatore la lascia.
Il processo di assegnazione dei giocatori alla sala giochi desiderata è chiamato "match-making". Colyseus semplifica la configurazione utilizzando il metodo filterBy
quando si definisce una nuova stanza:
gameServer.define("autowuzzler", AutowuzzlerRoom).filterBy(['gamePIN']);
Ora, tutti i giocatori che si uniscono al gioco con lo stesso gamePIN
(vedremo come "unirsi" più avanti) finiranno nella stessa sala giochi! Eventuali aggiornamenti di stato e altri messaggi trasmessi sono limitati ai giocatori nella stessa stanza.
Fisica in un'app Colyseus
Colyseus offre molto pronto all'uso per iniziare a funzionare rapidamente con un server di gioco autorevole, ma lascia allo sviluppatore la possibilità di creare le effettive meccaniche di gioco, inclusa la fisica. Phaser.js, che ho usato nel prototipo, non può essere eseguito in un ambiente non browser, ma il motore fisico integrato di Phaser.js Matter.js può essere eseguito su Node.js.
Con Matter.js, definisci un mondo fisico con determinate proprietà fisiche come le sue dimensioni e gravità. Fornisce diversi metodi per creare oggetti della fisica primitiva che interagiscono tra loro aderendo a leggi (simulate) della fisica, inclusi massa, collisioni, movimento con attrito e così via. Puoi spostare gli oggetti applicando la forza , proprio come faresti nel mondo reale.
Un "mondo" di Matter.js è al centro del gioco Autowuzzler ; definisce la velocità con cui si muovono le macchine, quanto dovrebbe essere rimbalzante la palla, dove si trovano le porte e cosa succede se qualcuno tira una porta.
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]);
Codice semplificato per aggiungere un oggetto di gioco "palla" allo stage in Matter.js.
Una volta definite le regole, Matter.js può essere eseguito con o senza eseguire effettivamente il rendering di qualcosa su uno schermo. Per Autowuzzler , sto utilizzando questa funzione per riutilizzare il codice del mondo fisico sia per il server che per il client, con diverse differenze chiave:
Il mondo della fisica sul server :
- riceve l'input dell'utente (eventi della tastiera per guidare un'auto) tramite Colyseus e applica la forza appropriata sull'oggetto del gioco (l'auto dell'utente);
- esegue tutti i calcoli di fisica per tutti gli oggetti (giocatori e palla), compreso il rilevamento delle collisioni;
- comunica lo stato aggiornato di ogni oggetto di gioco a Colyseus, che a sua volta lo trasmette ai client;
- viene aggiornato ogni 16,6 millisecondi (= 60 fotogrammi al secondo), attivato dal nostro server Colyseus.
Il mondo della fisica sul cliente :
- non manipola direttamente gli oggetti di gioco;
- riceve lo stato aggiornato per ogni oggetto di gioco da Colyseus;
- applica le modifiche di posizione, velocità e angolo dopo aver ricevuto lo stato aggiornato;
- invia l'input dell'utente (eventi della tastiera per guidare un'auto) a Colyseus;
- carica gli sprite di gioco e usa un renderer per disegnare il mondo della fisica su un elemento canvas;
- salta il rilevamento delle collisioni (usando l'opzione
isSensor
per gli oggetti); - aggiornamenti utilizzando requestAnimationFrame, idealmente a 60 fps.
Ora, con tutta la magia che accade sul server, il client gestisce solo l'input e disegna sullo schermo lo stato che riceve dal server. Con un'eccezione:
Interpolazione sul cliente
Poiché stiamo riutilizzando lo stesso mondo fisico di Matter.js sul client, possiamo migliorare le prestazioni dell'esperienza con un semplice trucco. Invece di aggiornare solo la posizione di un oggetto di gioco, sincronizzeremo anche la velocità dell'oggetto . In questo modo, l'oggetto continua a muoversi lungo la sua traiettoria anche se il prossimo aggiornamento dal server richiede più tempo del solito. Quindi, invece di spostare gli oggetti in passi discreti dalla posizione A alla posizione B, cambiamo la loro posizione e li facciamo muovere in una certa direzione.
Ciclo vitale
La classe Autowuzzler Room
è il luogo in cui viene gestita la logica relativa alle diverse fasi di una stanza Colyseus. Colyseus fornisce diversi metodi per il ciclo di vita:
-
onCreate
: quando viene creata una nuova stanza (di solito quando il primo client si connette); -
onAuth
: come gancio di autorizzazione a consentire o negare l'ingresso alla camera; -
onJoin
: quando un client si connette alla stanza; -
onLeave
: quando un cliente si disconnette dalla stanza; -
onDispose
: quando la stanza viene scartata.
La stanza di Autowuzzler crea una nuova istanza del mondo della fisica (vedi la sezione "Fisica in un'app Colyseus") non appena viene creata ( onCreate
) e aggiunge un giocatore al mondo quando un client si connette ( onJoin
). Quindi aggiorna il mondo della fisica 60 volte al secondo (ogni 16,6 millisecondi) usando il metodo setSimulationInterval
(il nostro ciclo di gioco principale):
// deltaTime is roughly 16.6 milliseconds this.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));
Gli oggetti della fisica sono indipendenti dagli oggetti Colyseus, il che ci lascia con due permutazioni dello stesso oggetto di gioco (come la palla), ovvero un oggetto nel mondo della fisica e un oggetto Colyseus che può essere sincronizzato.
Non appena l'oggetto fisico cambia, le sue proprietà aggiornate devono essere riapplicate all'oggetto Colyseus. Possiamo ottenerlo ascoltando l'evento afterUpdate di afterUpdate
e impostando i valori da 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 })
C'è un'altra copia degli oggetti di cui dobbiamo prenderci cura: gli oggetti di gioco nel gioco rivolto all'utente .
Applicazione lato client
Ora che abbiamo un'applicazione sul server che gestisce la sincronizzazione dello stato di gioco per più stanze così come i calcoli di fisica, concentriamoci sulla costruzione del sito web e sull'effettiva interfaccia di gioco . Il frontend di Autowuzzler ha le seguenti responsabilità:
- consente agli utenti di creare e condividere PIN di gioco per accedere alle singole stanze;
- invia i PIN di gioco creati a un database Supabase per la persistenza;
- fornisce una pagina opzionale "Partecipa a una partita" per consentire ai giocatori di inserire il PIN di gioco;
- convalida i PIN di gioco quando un giocatore si unisce a un gioco;
- ospita e rende il gioco reale su un URL condivisibile (cioè univoco);
- si connette al server Colyseus e gestisce gli aggiornamenti di stato;
- fornisce una pagina di destinazione ("marketing").
Per l'implementazione di tali attività, ho scelto SvelteKit su Next.js per i seguenti motivi:
Perché SvelteKit?
Volevo sviluppare un'altra app usando Svelte da quando ho creato neolightsout. Quando SvelteKit (il framework dell'applicazione ufficiale per Svelte) è entrato in beta pubblica, ho deciso di creare Autowuzzler con esso e accettare qualsiasi mal di testa derivante dall'utilizzo di una nuova versione beta: la gioia di usare Svelte lo compensa chiaramente.
Queste caratteristiche chiave mi hanno fatto scegliere SvelteKit su Next.js per l'effettiva implementazione del frontend di gioco:
- Svelte è un framework dell'interfaccia utente e un compilatore e quindi fornisce codice minimo senza runtime client;
- Svelte ha un linguaggio di modellazione espressivo e un sistema di componenti (preferenza personale);
- Svelte include negozi globali, transizioni e animazioni pronte all'uso, il che significa: nessun affaticamento decisionale nella scelta di un kit di strumenti per la gestione dello stato globale e una libreria di animazioni;
- Svelte supporta CSS con ambito in componenti a file singolo;
- SvelteKit supporta SSR, routing basato su file semplice ma flessibile e percorsi lato server per la creazione di un'API;
- SvelteKit consente a ciascuna pagina di eseguire codice sul server, ad esempio per recuperare i dati utilizzati per il rendering della pagina;
- Layout condivisi tra percorsi;
- SvelteKit può essere eseguito in un ambiente serverless.
Creazione e memorizzazione di PIN di gioco
Prima che un utente possa iniziare a giocare, deve prima creare un PIN di gioco. Condividendo il PIN con altri, possono accedere tutti alla stessa sala giochi.
Questo è un ottimo caso d'uso per gli endpoint lato server di SvelteKits in combinazione con la funzione Sveltes onMount: l'endpoint /api/createcode
genera un PIN di gioco, lo memorizza in un database Supabase.io e restituisce il PIN di gioco come risposta . Questa risposta viene recuperata non appena il componente di pagina della pagina "crea" viene montato:
Memorizzazione dei PIN di gioco con Supabase.io
Supabase.io è un'alternativa open source a Firebase. Supabase rende molto semplice creare un database PostgreSQL e accedervi tramite una delle sue librerie client o tramite REST.
Per il client JavaScript, importiamo la funzione createClient
e la eseguiamo utilizzando i parametri supabase_url
e supabase_key
ricevuti durante la creazione del database. Per memorizzare il PIN di gioco creato su ogni chiamata all'endpoint createcode
, tutto ciò che dobbiamo fare è eseguire questa semplice query di 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 }]);
Nota : supabase_url
e supabase_key
sono archiviati in un file .env. A causa di Vite, lo strumento di compilazione al centro di SvelteKit, è necessario anteporre alle variabili di ambiente VITE_ per renderle accessibili in SvelteKit.
Accesso al gioco
Volevo rendere l'adesione a un gioco di Autowuzzler facile come seguire un collegamento. Pertanto, ogni sala giochi doveva avere il proprio URL basato sul PIN di gioco creato in precedenza , ad esempio https://autowuzzler.com/play/12345.
In SvelteKit, le pagine con parametri di percorso dinamici vengono create mettendo le parti dinamiche del percorso tra parentesi quadre quando si nomina il file di paging: client/src/routes/play/[gamePIN].svelte
. Il valore del parametro gamePIN
diventerà quindi disponibile nel componente della pagina (consultare i documenti SvelteKit per i dettagli). Nel percorso di play
, dobbiamo connetterci al server Colyseus, creare un'istanza del mondo della fisica per renderizzare sullo schermo, gestire gli aggiornamenti degli oggetti di gioco, ascoltare l'input della tastiera e visualizzare altre UI come il punteggio e così via.
Connessione a Colyseus e aggiornamento dello stato
La libreria client Colyseus ci consente di connettere un client a un server Colyseus. Per prima cosa, creiamo un nuovo Colyseus.Client
puntandolo al server Colyseus ( ws://localhost:2567
in sviluppo). Quindi unisciti alla stanza con il nome che abbiamo scelto in precedenza ( autowuzzler
) e il gamePIN
dal parametro route. Il parametro gamePIN
assicura che l'utente si unisca all'istanza della stanza corretta (vedi "matchmaking" sopra).
let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN });
Poiché SvelteKit esegue inizialmente il rendering delle pagine sul server, è necessario assicurarsi che questo codice venga eseguito sul client solo dopo che la pagina è stata caricata. Ancora una volta, utilizziamo la funzione del ciclo di vita onMount
per quel caso d'uso. (Se hai familiarità con React, onMount
è simile useEffect
con un array di dipendenze vuoto.)
onMount(async () => { let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN }); })
Ora che siamo connessi al server di gioco Colyseus, possiamo iniziare ad ascoltare eventuali modifiche ai nostri oggetti di gioco.
Ecco un esempio di come ascoltare un giocatore che si unisce alla stanza ( onAdd
) e riceve aggiornamenti di stato consecutivi su questo giocatore:
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 }); }; };
Nel metodo updatePlayer
del mondo della fisica, aggiorniamo le proprietà una per una perché onChange
di Colyseus fornisce un insieme di tutte le proprietà modificate.
Nota : questa funzione viene eseguita solo sulla versione client del mondo fisico, poiché gli oggetti di gioco vengono manipolati solo indirettamente tramite il server 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 stessa procedura vale per gli altri oggetti di gioco (palla e squadre): ascolta i loro cambiamenti e applica i valori modificati al mondo fisico del cliente.
Finora, nessun oggetto si sta muovendo perché abbiamo ancora bisogno di ascoltare l'input della tastiera e inviarlo al server . Invece di inviare direttamente eventi su ogni evento keydown
, manteniamo una mappa dei tasti attualmente premuti e inviamo eventi al server Colyseus in un ciclo di 50 ms. In questo modo, possiamo supportare la pressione di più tasti contemporaneamente e mitigare la pausa che si verifica dopo il primo e consecutivo keydown
quando il tasto rimane premuto:
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);
Ora il ciclo è completo: ascolta le sequenze di tasti, invia i comandi corrispondenti al server Colyseus per manipolare il mondo fisico sul server. Il server Colyseus applica quindi le nuove proprietà fisiche a tutti gli oggetti di gioco e propaga i dati al client per aggiornare l'istanza del gioco rivolta all'utente.
Piccoli fastidi
In retrospettiva, due cose della categoria nessuno-me l'ha detto-ma-qualcuno-avrebbe dovuto :
- Una buona comprensione di come funzionano i motori fisici è utile. Ho passato molto tempo a mettere a punto proprietà e vincoli della fisica. Anche se in precedenza avevo creato un piccolo gioco con Phaser.js e Matter.js, c'erano molti tentativi ed errori per far muovere gli oggetti nel modo in cui li immaginavo.
- Il tempo reale è difficile , specialmente nei giochi basati sulla fisica. Ritardi minori peggiorano considerevolmente l'esperienza e, sebbene la sincronizzazione dello stato tra i client con Colyseus funzioni alla grande, non è possibile rimuovere i ritardi di calcolo e trasmissione.
Problemi e avvertenze con SvelteKit
Dato che ho usato SvelteKit quando era appena uscito dal forno beta, c'erano alcuni problemi e avvertenze che vorrei sottolineare:
- Ci è voluto del tempo per capire che le variabili di ambiente devono essere precedute da VITE_ per poterle utilizzare in SvelteKit. Questo è ora adeguatamente documentato nelle FAQ.
- Per utilizzare Supabase, ho dovuto aggiungere Supabase sia alle
dependencies
che agli elenchidevDependencies
di package.json. Credo che non sia più così. - La funzione di
load
SvelteKits viene eseguita sia sul server che sul client! - Per abilitare la sostituzione completa del modulo a caldo (incluso il mantenimento dello stato), devi aggiungere manualmente una riga di commento
<!-- @hmr:keep-all -->
nei componenti della tua pagina. Vedere le domande frequenti per maggiori dettagli.
Anche molti altri framework sarebbero stati fantastici, ma non ho rimpianti per aver scelto SvelteKit per questo progetto. Mi ha permesso di lavorare sull'applicazione client in modo molto efficiente, principalmente perché Svelte stesso è molto espressivo e salta molto del codice standard, ma anche perché Svelte ha cose come animazioni, transizioni, CSS con ambito e negozi globali integrati. SvelteKit ha fornito tutti gli elementi costitutivi di cui avevo bisogno (SSR, routing, percorsi del server) e sebbene fosse ancora in versione beta, sembrava molto stabile e veloce.
Distribuzione e hosting
Inizialmente, ho ospitato il server Colyseus (Node) su un'istanza Heroku e ho perso molto tempo a far funzionare WebSocket e CORS. A quanto pare, le prestazioni di un piccolo banco di prova (gratuito) di Heroku non sono sufficienti per un caso d'uso in tempo reale. Successivamente ho migrato l'app Colyseus su un piccolo server su Linode. L'applicazione lato client viene distribuita e ospitata su Netlify tramite l'adattatore SvelteKits-netlify. Nessuna sorpresa qui: Netlify ha funzionato alla grande!
Conclusione
Iniziare con un prototipo davvero semplice per convalidare l'idea mi ha aiutato molto a capire se vale la pena seguire il progetto e quali sono le sfide tecniche del gioco. Nell'implementazione finale, Colyseus si è occupato di tutto il lavoro pesante della sincronizzazione dello stato in tempo reale su più client, distribuiti in più stanze. È impressionante la rapidità con cui un'applicazione multiutente in tempo reale può essere creata con Colyseus, una volta capito come descrivere correttamente lo schema. Il pannello di monitoraggio integrato di Colyseus aiuta a risolvere eventuali problemi di sincronizzazione.
Ciò che ha complicato questa configurazione è stato il livello fisico del gioco perché ha introdotto una copia aggiuntiva di ogni oggetto di gioco relativo alla fisica che doveva essere mantenuto. La memorizzazione dei PIN di gioco in Supabase.io dall'app SvelteKit è stata molto semplice. Con il senno di poi, avrei potuto semplicemente utilizzare un database SQLite per archiviare i PIN di gioco, ma provare nuove cose è metà del divertimento quando si creano progetti collaterali.
Infine, l'utilizzo di SvelteKit per creare il frontend del gioco mi ha permesso di muovermi rapidamente e con qualche sorriso di gioia sul viso.
Ora vai avanti e invita i tuoi amici a un round di Autowuzzler!
Ulteriori letture su Smashing Magazine
- "Inizia con React costruendo un gioco Whac-A-Mole", Jhey Tompkins
- "Come costruire un gioco di realtà virtuale multiplayer in tempo reale", Alvin Wan
- "Scrivere un motore di avventura testuale multigiocatore in Node.js", Fernando Doglio
- "Il futuro del web design mobile: design di videogiochi e narrazione", Suzanne Scacca
- "Come costruire un gioco di corridori senza fine nella realtà virtuale", Alvin Wan