Nakama e Open Match: che cosa ti danno davvero, e che cosa devi mettere tu
Se stai scegliendo il backend di un gioco multiplayer, hai tre strade: scrivere tutto, comprare un servizio gestito, oppure partire da un progetto open source. Nakama e Open Match sono i due nomi che ricorrono di più nella terza strada, e vengono spesso presentati come un'unica soluzione. Non lo sono. Nakama (di Heroic Labs) è un server di gioco completo: account, archivio dei dati dei giocatori, amici, chat, classifiche e un motore per partite in tempo reale. Open Match (di Google) fa una cosa sola, il matchmaking, e non sa nulla di account né di partite.
La differenza che decide tutto il resto è questa: la versione open source di Nakama gira come un solo nodo. Il cluster è una funzione della versione Enterprise. Chi legge «scala orizzontalmente su Kubernetes» e mette tre repliche dietro un servizio sta costruendo qualcosa che la documentazione non descrive. L'articolo parte da qui: che cosa fa ciascun progetto, il codice di una partita autoritativa che puoi eseguire senza server, il modo in cui i due si collegano, e dove si fanno male i team che li scelgono per il motivo sbagliato.
Prerequisito: il modello client-server autoritativo spiegato in Anatomia di un game backend multiplayer. Il matchmaking come problema (code, rating, attese) è in Sistema di matchmaking con Elo, Glicko-2 e code.
Che cosa c'è dentro Nakama
Nakama è un unico programma in Go che espone un'API gRPC e HTTP e una console di amministrazione. Il suo
archivio è un database relazionale: la pagina di configurazione ufficiale dichiara che richiede un'istanza
CockroachDB, con indirizzo predefinito root@localhost:26257, e che crea e gestisce il proprio
database nakama.
- Identità: autenticazione con email, identificativo del dispositivo e provider sociali, più sessioni
- Archivio a collezioni: oggetti JSON per giocatore con permessi di lettura e scrittura distinti
- Sociale: amici, gruppi, chat in tempo reale, notifiche
- Competitivo: classifiche e tornei
- Multiplayer: partite relay (il server inoltra i messaggi), partite autoritative (il server calcola lo stato) e un matchmaker integrato
L'elenco ha un ordine di importanza per chi sceglie: identità e archivio sono ciò che quasi ogni gioco con account deve avere comunque, e Nakama li dà pronti. Le partite autoritative sono la parte che ti permette di applicare l'idea di autorità del server contro i cheat. Il matchmaker integrato esiste: Open Match serve quando la sua logica non basta, non come condizione per avere un matchmaking.
Come si estende: un runtime TypeScript che non è Node
Per aggiungere logica a Nakama scrivi un modulo di runtime in Go, TypeScript/JavaScript o Lua. Qui sta la prima
trappola. La documentazione del runtime JavaScript dice che gira su goja, una macchina virtuale
JavaScript scritta in Go, che supporta la specifica ES5, senza librerie che richiedano Node, API
del browser o codice nativo. Non c'è Deno, e quel che tipicamente dai per scontato in TypeScript moderno
(Map, Math.sign, Array.from) va controllato prima di usarlo. Inoltre le
funzioni passate all'Initializer devono stare nello scope globale, e InitModule è il
punto d'ingresso.
Per non fidarmi della memoria ho fatto così: ho scaricato i tipi ufficiali (nakama-runtime, dal
repository heroiclabs/nakama-common), ho scritto il modulo con "target": "ES5" e
"lib": ["ES5"] in tsconfig.json, e ho lasciato che tsc in modalità
strict bocciasse tutto ciò che non esiste. Poi ho caricato il JavaScript prodotto in un contesto
vm di Node (uno scope globale unico, come in goja) e ho pilotato le funzioni con un finto
nk. Non è Nakama: è un modello didattico che prova la logica, non l'integrazione con il server.
Il primo blocco è il punto d'ingresso. registerMatch collega al nome game_match i sette
callback del ciclo di vita di una partita, e registerRpc espone una funzione che il client può
chiamare. Se un callback manca, tsc lo segnala: l'interfaccia MatchHandler li richiede
tutti e sette.
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
initializer.registerMatch<GameState>('game_match', {
matchInit: matchInit, matchJoinAttempt: matchJoinAttempt, matchJoin: matchJoin,
matchLeave: matchLeave, matchLoop: matchLoop, matchTerminate: matchTerminate, matchSignal: matchSignal,
});
initializer.registerRpc('start_matchmaking', startMatchmakingRpc);
logger.info('runtime caricato');
}
// Output:
// [log] runtime caricato
// registrati: [ 'game_match' ] [ 'start_matchmaking' ]
L'uscita mostra che dopo il caricamento risultano registrati una partita e una RPC. Se togli la riga del
registerRpc la seconda lista resta vuota e il client non ha nessuna funzione da chiamare.
La partita autoritativa, passo per passo
Una partita autoritativa è un ciclo che gira sul server a una frequenza fissa. Il tickRate che
ritorni da matchInit è il numero di chiamate a matchLoop al secondo; con 10, la
documentazione spiega che ogni ciclo parte 100 ms dopo l'inizio del precedente. Ad ogni chiamata ricevi la lista
dei messaggi arrivati dai client dall'ultimo tick: tu li convalidi, aggiorni lo stato e, se vuoi, trasmetti.
Lo stato vive in un solo nodo, quello che ospita la partita: è la ragione per cui non serve nessuna
sincronizzazione distribuita, e per cui una partita non si «sposta» su un'altra replica.
Nel codice del primo blocco, il client propone uno spostamento e il server decide. applyMove scarta
i numeri non finiti, limita ogni asse a MAX_STEP unità e tiene il giocatore dentro la mappa.
applyAttack non legge nulla dal messaggio tranne chi attacca: il danno è la costante
ATTACK_DAMAGE. È una scelta di sicurezza e non di comodità. Se il client potesse mandare
damage, ogni giocatore diventerebbe un bersaglio con un solo messaggio.
function applyMove(p: Player, dx: number, dy: number): void {
// ignora i numeri non finiti e limita il passo: il client propone, il server decide
if (!isFinite(dx) || !isFinite(dy)) { return; }
p.x = clamp(p.x + sign(dx) * Math.min(Math.abs(dx), MAX_STEP), 0, MAP_SIZE);
p.y = clamp(p.y + sign(dy) * Math.min(Math.abs(dy), MAX_STEP), 0, MAP_SIZE);
}
function applyAttack(state: GameState, attackerId: string): void {
var a = state.players[attackerId];
if (!a || !a.alive) { return; }
Object.keys(state.players).forEach(function (id) {
var t = state.players[id];
if (id === attackerId || !t.alive) { return; }
var dist = Math.sqrt(Math.pow(t.x - a.x, 2) + Math.pow(t.y - a.y, 2));
if (dist <= ATTACK_RANGE) {
t.health = Math.max(0, t.health - ATTACK_DAMAGE);
if (t.health === 0) { t.alive = false; a.score += 100; }
}
});
}
// Output:
// A dopo dx=9999, dy=-3: {"x":15,"y":47,"health":30,"score":0,"alive":true}
// A dopo 3 passi da 5: {"x":30,"y":47,"health":30,"score":0,"alive":true} | B: {"x":30,"y":50,"health":30,"score":0,"alive":true}
// B dopo un attacco con damage=9999 nel payload: {"x":30,"y":50,"health":20,"score":0,"alive":true}
Le tre righe di uscita provano le tre difese. Un messaggio con dx=9999 sposta il giocatore di 5
unità (da 10 a 15), tre passi lo portano a 30, e un attacco il cui contenuto dichiara damage: 9999 toglie
comunque 10 punti: la salute del bersaglio passa da 30 a 20. Nota anche il caso che non vedi: un movimento con
dx assente produce NaN, e isFinite lo scarta invece di corrompere la posizione.
Il ciclo vero e proprio fa quattro cose nell'ordine in cui conta. Decodifica ogni messaggio con
nk.binaryToString (il campo data è un ArrayBuffer, non una stringa) dentro un
try, perché il contenuto arriva dalla rete e può non essere JSON. Applica solo i messaggi dei giocatori vivi
e solo in fase di gioco. Calcola la fase successiva. Ritorna { state }, oppure
null per chiudere la partita: la forma dell'oggetto di ritorno è fissata dai tipi ufficiali, e se ritorni lo stato nudo il compilatore lo segnala subito.
var matchLoop: nkruntime.MatchLoopFunction<GameState> = function (ctx, logger, nk, dispatcher, tick, state, messages) {
messages.forEach(function (m) {
var p = state.players[m.sender.userId];
if (!p || !p.alive || state.phase !== 'playing') { return; }
var data: { dx?: number; dy?: number };
try { data = JSON.parse(nk.binaryToString(m.data)); } catch (e) { return; } // payload malformato: scartato
if (m.opCode === OP_MOVE) { applyMove(p, Number(data.dx), Number(data.dy)); }
if (m.opCode === OP_ATTACK) { applyAttack(state, m.sender.userId); }
});
var ids = Object.keys(state.players);
var alive = ids.filter(function (id) { return state.players[id].alive; });
if (state.phase === 'waiting' && alive.length >= 2) { state.phase = 'playing'; }
if (state.phase === 'playing' && (alive.length <= 1 || tick >= state.maxTicks)) {
state.phase = 'ended';
dispatcher.broadcastMessage(OP_RESULT, JSON.stringify(state.players), null, null, true);
return null; // restituire null chiude il match
}
return { state: state };
};
var matchTerminate: nkruntime.MatchTerminateFunction<GameState> = function (ctx, logger, nk, dispatcher, tick, state, grace) { return { state: state }; };
var matchSignal: nkruntime.MatchSignalFunction<GameState> = function (ctx, logger, nk, dispatcher, tick, state, data) { return { state: state, data: data }; };
// Output:
// payload malformato: nessuna eccezione, fase = playing
// match chiuso al tick 9 - messaggi finali: 1 op 99
Nella prova, il payload non e json non solleva eccezioni e la fase resta playing. Poi il
giocatore A attacca finché B non muore: con 30 punti di salute e 10 di danno servono tre colpi, e la partita si
chiude al tick 9 con un messaggio finale di codice 99. Il tempo è misurato in tick e non con
Date.now(): così la partita è riproducibile e un test non dipende dall'orologio.
La durata massima, TICK_RATE * 30 tick, vale 30 secondi a 10 tick al secondo.
Quando non è la scelta giusta
Se il gioco è a turni lenti o cooperativo e senza incentivi a barare, una partita relay o la sola archiviazione dei dati bastano e costano meno. La partita autoritativa ha un costo reale: CPU del server per ogni tick e il lavoro di scrivere la fisica due volte, nel client per la prontezza e nel server per la verità.
Come lo metti in produzione senza il cluster
La pagina sulle partite autoritative è esplicita: la partita e il suo stato stanno su un nodo, che ne diventa l'ospite, e solo la versione Enterprise replica le presenze tra nodi e bilancia automaticamente le partite. Nella pagina di configurazione la sezione del cluster è marcata «Nakama Enterprise Only». Per un team con la versione open source, ciò significa tre cose concrete.
- Un'istanza per ambiente: una sola replica del processo, con riavvio automatico, non un Deployment con tre repliche
- Scala verticale: più CPU e memoria sul nodo, e un database separato che regge i picchi di scrittura
- Più istanze indipendenti: se serve più capacità, istanze separate con database separati, ciascuna con i propri giocatori (per regione, per esempio), e non un'unica istanza distribuita
La terza voce è quella che cambia l'architettura: se i giocatori di due istanze devono incontrarsi, il matchmaking e l'identità devono stare sopra, e lì entra in gioco un livello esterno. Non pubblico qui un manifest Kubernetes: non posso eseguirlo, e lo schema più comune, con tre repliche e un autoscaler, promette un comportamento che questa edizione non ha. Il confine fra ciò che Nakama risolve e ciò che resta tuo lo trovi anche in Game server dedicati: GameLift e Agones, che copre l'orchestrazione dei server di gioco a parte.
Open Match: il matchmaking a pezzi che scrivi tu
Open Match è un framework in Go con licenza Apache 2.0, pensato per Kubernetes. La documentazione ufficiale è alla release 1.8.0 e ha un'area separata per Open Match 2. Non è un matchmaker pronto: ti dà il piano di controllo (raccolta dei ticket, ricerca, sincronizzazione) e ti chiede di scrivere due componenti.
- Frontend: riceve i ticket dai giocatori, via gRPC o via HTTP (porta 51504 nell'esempio della guida)
- Query: permette alla match function di leggere i ticket che soddisfano i filtri di un profilo
- Match function (tua): servizio gRPC o HTTP con un metodo
Run: riceve unMatchProfilee restituisce in streaming le proposte di partita - Backend: espone
FetchMatcheseAssignTicketsal tuo Director - Director (tuo): un processo che chiede le partite, ottiene un server di gioco e assegna i ticket
- Evaluator e Synchronizer: risolvono le proposte in conflitto, quando lo stesso ticket finisce in due partite; c'è un valutatore predefinito
Il Director non è un servizio che Open Match ti consegna: la guida lo presenta come il processo che chiede le partite e assegna i server, e lo scrivi tu. Scrivere Director e match function è dunque il lavoro, e questo significa che Open Match non ti libera dal progettare l'algoritmo di abbinamento: ti libera dal gestire lo stato dei ticket su larga scala.
Il Director, nella versione Go della guida, è un ciclo che chiama FetchMatches con l'indirizzo della
match function, riceve le partite in streaming, ottiene dal tuo sistema di allocazione la stringa di connessione
del server e la associa ai ticket con AssignTickets. Non l'ho eseguito, perché Go non è installato
nella macchina in cui ho lavorato: i nomi di metodi e messaggi sono quelli della pagina del Director, ma
va considerato uno schema da adattare e da compilare nel tuo progetto.
// Director (Go): schema da adattare, NON eseguito (Go non è installato sulla macchina di lavoro)
package main
import (
"context"
"io"
"open-match.dev/open-match/pkg/pb"
)
func fetchAndAssign(ctx context.Context, be pb.BackendServiceClient, profile *pb.MatchProfile) error {
stream, err := be.FetchMatches(ctx, &pb.FetchMatchesRequest{
Config: &pb.FunctionConfig{Host: "matchfunction", Port: 50502, Type: pb.FunctionConfig_GRPC},
Profile: profile,
})
if err != nil {
return err
}
for {
resp, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
conn := allocateGameServer(resp.GetMatch()) // a carico tuo: Agones, un'API di allocazione, un tuo pool
ids := make([]string, 0, len(resp.GetMatch().GetTickets()))
for _, t := range resp.GetMatch().GetTickets() {
ids = append(ids, t.GetId())
}
_, err = be.AssignTickets(ctx, &pb.AssignTicketsRequest{
Assignments: []*pb.AssignmentGroup{{TicketIds: ids, Assignment: &pb.Assignment{Connection: conn}}},
})
if err != nil {
return err
}
}
}
Il pezzo che non è un dettaglio è allocateGameServer: Open Match non crea né trova server di gioco.
La guida dice che il Director può prenderne uno dal sistema di allocazione e impostarlo come assegnazione sui
ticket. In pratica quel sistema è Agones, un servizio tuo o un pool fisso: lo vedi nell'articolo
sull'orchestrazione linkato sopra. Un errore di assegnazione lascia i ticket in attesa: la funzione ritorna
l'errore e il chiamante deve decidere se ritentare.
La logica di abbinamento: che cosa fa una match function
La match function riceve il profilo con i suoi pool (gruppi di ticket che soddisfano un filtro, per
esempio un intervallo di MMR) e propone partite. Il compito vero è un compromesso fra qualità e attesa. Con
un'ampiezza massima di rating stretta le partite sono eque ma qualcuno aspetta; con una larga nessuno aspetta ma
le partite sono sbilanciate. La guida suggerisce di recuperare i ticket con la libreria
QueryPools e di mandare le proposte in streaming. Poiché non eseguo Go, ho riscritto la sola logica di
raggruppamento in JavaScript, con un seme fisso, come modello didattico.
// Modello didattico della logica di una match function: raggruppa ticket per MMR vicino.
function seeded(seed) { let s = seed; return () => (s = (s * 1664525 + 1013904223) % 4294967296) / 4294967296; }
function proposeMatches(tickets, size, maxSpread) {
const sorted = [...tickets].sort((a, b) => a.mmr - b.mmr);
const proposals = [];
let i = 0;
while (i + size <= sorted.length) {
const group = sorted.slice(i, i + size);
const spread = group[size - 1].mmr - group[0].mmr;
if (spread <= maxSpread) { proposals.push({ tickets: group.map(t => t.id), spread }); i += size; }
else { i += 1; } // il ticket più basso non entra in questo gruppo: si prova dal successivo
}
const used = new Set(proposals.flatMap(p => p.tickets));
return { proposals, left: sorted.filter(t => !used.has(t.id)).map(t => t.id) };
}
const rnd = seeded(7);
const tickets = Array.from({ length: 20 }, (_, n) => ({ id: 't' + n, mmr: Math.round(800 + rnd() * 1200) }));
console.log('mmr:', tickets.map(t => t.mmr).sort((a, b) => a - b).join(' '));
for (const spread of [200, 400]) {
const r = proposeMatches(tickets, 4, spread);
console.log(`spread max ${spread}: ${r.proposals.length} partite, ${r.left.length} ticket in attesa`,
r.proposals.map(p => p.spread));
}
// Output:
// mmr: 859 864 883 1087 1233 1302 1342 1369 1438 1457 1526 1535 1733 1736 1823 1827 1896 1912 1967 1981
// spread max 200: 4 partite, 4 ticket in attesa [ 136, 97, 94, 85 ]
// spread max 400: 5 partite, 0 ticket in attesa [ 228, 136, 97, 94, 85 ]
Con 20 giocatori sintetici e partite da 4, un'ampiezza massima di 200 punti di MMR produce 4 partite e lascia 4 ticket in attesa; con 400 si formano 5 partite e nessuno aspetta, ma la prima mette insieme giocatori con 228 punti di scarto. Le cifre vengono da questo generatore, non da un caso reale: servono a mostrare che il parametro non ha un valore giusto in assoluto. La scelta dipende da quanto traffico hai: con poche code attive un'ampiezza stretta lascia i giocatori in attesa per sempre, quindi in produzione l'ampiezza cresce con il tempo passato in coda. Come si calcola il rating, per esempio con Glicko-2, è un altro argomento: qui il rating è un numero già dato.
Far parlare Nakama e Open Match
I due sistemi si collegano con un ponte sottile. Il client chiede a Nakama di cercare una partita; una RPC di
Nakama legge il rating dall'archivio, crea un ticket sul frontend di Open Match e salva l'identificativo
del ticket per il giocatore. Quando il Director assegna il server, il client lo scopre interrogando il ticket (la guida del frontend elenca un GET su
/v1/frontendservice/tickets/{ticket_id} e uno streaming WatchAssignments) e si
connette. Il percorso del frontend per creare un ticket è /v1/frontendservice/tickets e il corpo ha
un campo ticket: è così nella guida del frontend.
/// <reference path="../node_modules/nakama-runtime/index.d.ts" />
var OPEN_MATCH_FRONTEND = 'http://open-match-frontend:51504/v1/frontendservice/tickets';
var startMatchmakingRpc: nkruntime.RpcFunction = function (ctx, logger, nk, payload) {
if (!ctx.userId) { throw new Error('serve una sessione autenticata'); }
var userId = ctx.userId;
var req = JSON.parse(payload) as { mode: string; region: string };
var rows = nk.storageRead([{ collection: 'player_stats', key: 'rating', userId: userId }]);
var mmr = rows.length > 0 ? (rows[0].value as { mmr: number }).mmr : 1000;
var body = JSON.stringify({
ticket: { search_fields: {
double_args: { mmr: mmr },
string_args: { mode: req.mode, region: req.region },
tags: ['player.' + userId],
} },
});
var res = nk.httpRequest(OPEN_MATCH_FRONTEND, 'post', { 'Content-Type': 'application/json' }, body, 5000);
if (res.code !== 200) { throw new Error('Open Match ha risposto ' + res.code); }
var ticketId = (JSON.parse(res.body) as { id: string }).id;
nk.storageWrite([{ collection: 'matchmaking', key: 'current_ticket', userId: userId,
value: { ticket_id: ticketId, mode: req.mode }, permissionRead: 1, permissionWrite: 0 }]);
return JSON.stringify({ ticket_id: ticketId, status: 'searching' });
};
// Output:
// POST http://open-match-frontend:51504/v1/frontendservice/tickets
// {"ticket":{"search_fields":{"double_args":{"mmr":1480},"string_args":{"mode":"deathmatch","region":"eu"},"tags":["player.A"]}}}
// {"ticket_id":"t-42","status":"searching"}
// salvato: {"ticket_id":"t-42","mode":"deathmatch"}
// senza sessione: serve una sessione autenticata
L'uscita dimostra la forma della richiesta e la gestione del caso senza sessione. Tre righe del codice meritano
una spiegazione. search_fields contiene i campi su cui i filtri del profilo cercano: il rating come
double_args, modalità e regione come string_args. Il timeout di 5000 ms dentro
httpRequest evita che una RPC resti appesa se il frontend non risponde. E se Open Match non risponde
con 200, la funzione lancia un errore invece di salvare un ticket che non esiste. Non ho potuto eseguire la
chiamata contro un Open Match vero (nessun server né cluster in questa macchina): la prova usa un
httpRequest finto, quindi la forma del JSON è quella della guida, non una risposta osservata.
Open source o servizio gestito: la scelta onesta
Una tabella di costi per milione di utenti attivi, come ne circolano tante, non regge: dipende da quanto tempo di ingegneri conta e da come si valuta l'attesa. Meglio ragionare per tre domande, che ti dicono cosa stai davvero comprando.
- Chi gestisce il database e i picchi? Con Nakama open source: tu, e un'istanza sola è un punto unico di guasto
- Quanto devi cambiare le regole? Se matchmaking e logica di partita sono il tuo gioco, il codice sorgente a disposizione conta più di ogni funzione già pronta
- Dove stanno i dati? Con un'installazione tua decidi tu la regione e la retention; con un servizio gestito vale il contratto del fornitore, da leggere
Se rispondi «non voglio gestire un database e un nodo di produzione» a una sola delle tre, un servizio gestito è onesto e rapido. Se rispondi «le regole sono il mio prodotto» a una sola, il costo operativo di Nakama e Open Match è un prezzo ragionevole. Una via di mezzo che funziona: parti con un servizio gestito per validare il gioco e migra dopo, tenendo l'accesso ai dati dietro un'interfaccia tua. Le operazioni live (eventi, feature flag) sono nell'articolo successivo della serie.
Domande frequenti
Nakama open source si può mettere in cluster?
No, secondo la documentazione ufficiale: la sezione del cluster e la replica delle presenze tra nodi sono marcate «Nakama Enterprise Only». Con la versione open source si scala in verticale o con istanze indipendenti.
Il runtime TypeScript di Nakama usa Node o Deno?
Nessuno dei due: gira su goja, una macchina virtuale JavaScript in Go che supporta ES5. Niente librerie che richiedono Node o API del browser, e le funzioni passate all'Initializer devono stare nello scope globale.
Open Match è un matchmaker pronto all'uso?
No. Fornisce frontend, backend e query; la match function e il Director li scrivi tu. Ti toglie il peso dello stato dei ticket su larga scala, non la progettazione dell'algoritmo di abbinamento.
Che cosa sai fare adesso
Sai distinguere ciò che Nakama copre (account, archivio, chat, partite autoritative) da ciò che Open Match aggiunge solo sul matchmaking, e sai perché la versione open source non è un cluster. Hai un modulo di partita scritto per ES5, controllato dai tipi ufficiali e provato con un finto server, in cui il client propone e il server decide. Sai come un ticket nasce da una RPC e come l'assegnazione torna al giocatore. Prima di impegnarti, prova il tuo caso con tre partite vere su un'istanza singola: ti dirà più di qualunque tabella di confronto.
Fonti
- Nakama: TypeScript Runtime - goja, ES5, assenza di librerie Node e funzioni nello scope globale, consultata il 2026-11-12
- Nakama: Authoritative Multiplayer - ciclo di vita della partita, tick rate, partita su un solo nodo, replica delle presenze solo in Enterprise, consultata il 2026-11-12
- Nakama: Configuration - database CockroachDB richiesto e indirizzo predefinito, sezione cluster solo Enterprise, consultata il 2026-11-12
- heroiclabs/nakama-common (pacchetto nakama-runtime) - le definizioni di tipo usate per compilare il modulo, consultata il 2026-11-12
- Open Match: Director - FetchMatches, AssignTickets e il ruolo del Director, consultata il 2026-11-12
- Open Match: Game Frontend - percorsi HTTP per creare, leggere e osservare un ticket, consultata il 2026-11-12
- Open Match: Match Function - il metodo Run, QueryPools e le proposte in streaming, consultata il 2026-11-12
- googleforgames/open-match - licenza Apache 2.0 e stato di sviluppo del progetto, consultata il 2026-11-12
Il modulo TypeScript è stato compilato con tsc 5 (target ES5, strict) contro i tipi di nakama-runtime ed eseguito in un contesto vm di Node 24 con un finto nk; il raggruppamento per MMR è un modello didattico con seme fisso. Il Director in Go è uno schema da adattare, non eseguito. Nessuna chiamata a un Nakama o a un Open Match reali.

Commenti
Caricamento commenti...
Accedi per commentare