Açık Maç ve Nakama: Açık Kaynaklı Oyun Arka Ucu
Sıfırdan bir oyun arka ucu oluşturmak, yıllar süren ve onlarca mühendisin çalışmasını gerektiren bir projedir. PlayFab, GameSparks, GameLift: Yönetilen bulut çözümleri her şeyi hazır sunuyor ancak çoğu kişi için bu maliyete mal oluyor bağımsız stüdyolar ve küçük şirketler yasaklayıcıdır, satıcıya bağımlı olmak yön değiştirmeyi zorlaştırır bir kez üstlenildi. Üçüncü bir yol daha var: açık kaynaklı çerçeveler.
Nakama Heroic Labs tarafından Açık Maç Google iki projedir Bu da sektörde oyunun kurallarını değiştirdi. Nakama eksiksiz bir sosyal oyun sunucusudur ve topluluğu tarafından ayda 1 trilyondan fazla isteğin işlendiği gerçek zamanlı çok oyunculu oyun. Open Match, Google Stadia (RIP) ve birçok AAA tarafından kullanılan esnek bir eşleştirme çerçevesidir stüdyolar. Her ikisi de Apache 2.0 lisanslıdır, maksimum performans için Go'da yazılmıştır ve tasarlanmıştır. Kubernetes'te çalıştırmak için.
Bu makalede her ikisini de derinlemesine inceleyeceğiz: iç mimari, Kubernetes dağıtımı, TypeScript/Go ile özelleştirme, ikisi arasında entegrasyon ve eksiksiz bir oyun vaka çalışması tamamen açık kaynak yığını üzerine inşa edilmiş çok oyunculu.
Ne Öğreneceksiniz
- Nakama mimarisi: temel özellikler, depolama, gerçek zamanlı, sosyal
- TypeScript ve Go'da Nakama özel sunucu mantığı (çalışma zamanı kancaları)
- Nakama'nın CockroachDB ve Redis ile Kubernetes'te Dağıtımı
- Açık Maç mimarisi: yönetmen, eşleştirme işlevi, değerlendirici
- Glicko-2 ile özel eşleştirme işlevi uygulaması
- Tam sistem için Nakama + Open Match entegrasyonu
- Yönetilen çözümlerle karşılaştırma: ne zaman neyi seçmeli
- Açık kaynak yığınının izlenmesi ve gözlemlenebilirliği
1. Nakama: Sosyal Oyun için Açık Kaynak Sunucusu
Nakama, 2017 yılında Heroic Labs tarafından net bir hedefle doğdu: her stüdyoya bir sunucu sağlamak Daha önce aylarca süren özel geliştirme gerektiren tüm sosyal ve çok oyunculu özelliklere sahip bir oyun. Sonuç, bildirimsel bir API ve sisteme sahip son derece yüksek performanslı bir Go sunucusudur çekirdek kodu değiştirmeden her davranışı özelleştirmenize olanak tanıyan çalışma zamanı kancaları.
Nakama'nın Temel Özellikleri
| Kategori | Özellikler | Detay |
|---|---|---|
| Yetki | Çoklu sağlayıcı kimlik doğrulaması | E-posta, cihaz kimliği, Apple, Google, Facebook, Steam, özel |
| Depolamak | Nesne depolama | Ayrıntılı ACL'lere sahip oyuncular için JSON mağazası (sahip/genel/arkadaşlar) |
| Gerçek zamanlı | Aktarılan çok oyunculu | Sunucu tarafı yetkilendirmesi ile maç rölesi, maç başına maksimum 64 oyuncu |
| Gerçek zamanlı | Yetkili eşleşmeler | Go/TypeScript/Lua'da özelleştirilebilir sunucu tarafı oyun döngüsü |
| Sosyal | Arkadaş sistemi | Arkadaş ekleme/kaldırma, engellenenler listesi, iletişim durumu (çevrimiçi/çevrimdışı/dışarıda) |
| Sosyal | Sohbet | 1'e 1, grup sohbeti, oda bazlı, mesaj kalıcılığıyla |
| Rekabetçi | Skor tabloları | Küresel, arkadaşlar, sezonluk, puan sahibi ve meta verilerle |
| Rekabetçi | Turnuvalar | Ödüllü süreli turnuvalar, katılım yetkisi |
| Ekonomi | Cüzdan | Atomik işlemlere sahip çok para birimli cüzdan |
| Bildirimler | Uygulama içi bildirimler | Kalıcılık ve okundu/okunmadı durumuyla anında bildirimler |
2. Nakama Çalışma Zamanı Kancaları: TypeScript'te Özelleştirme
Hooks çalışma zamanı sistemi Nakama'nın en güçlü özelliğidir. Herhangi bir şeyi engellemenizi sağlar sunucu olayı (kimlik doğrulama, depolama yazma, özel RPC çağrıları) ve mantık ekleme kişiselleştirilmiş. Çalışma zamanları Go (en iyi performans), TypeScript/JavaScript'te desteklenir (Deno aracılığıyla) ve Lua (tarihsel). TypeScript, yeni projeler için önerilen seçimdir.
// runtime/main.ts - Entry point del Nakama runtime TypeScript
// Importa l'interfaccia di Nakama per type safety
/// <reference types="@heroiclabs/nakama-runtime" />
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger,
nk: nkruntime.Nakama, initializer: nkruntime.Initializer): Error | void {
logger.info("Game runtime initialized - version 2.1.0");
// === REGISTRAZIONE BEFORE HOOKS ===
// Hook prima dell'autenticazione: validazione custom
initializer.registerBeforeAuthenticateEmail(
(ctx, logger, nk, request) => {
// Blocca email da domini blacklistati
const blockedDomains = ['tempmail.com', 'throwaway.email'];
const domain = request.email?.split('@')[1] ?? '';
if (blockedDomains.includes(domain)) {
throw new Error(`Email domain ${domain} is not allowed`);
}
return request; // Passa la richiesta inalterata
}
);
// Hook dopo la creazione account: inizializzazione wallet
initializer.registerAfterAuthenticateEmail(
(ctx, logger, nk, out, request) => {
if (out.created) {
// Nuovo giocatore: assegna valuta iniziale
try {
nk.walletUpdate(ctx.userId!, { coins: 500, gems: 10 },
{ reason: "welcome_bonus" }, true);
logger.info(`Welcome bonus assigned to ${ctx.userId}`);
} catch (e) {
logger.error(`Failed to assign welcome bonus: ${e}`);
}
}
}
);
// === REGISTRAZIONE RPC FUNCTIONS ===
// RPC: recupera statistiche giocatore
initializer.registerRpc('get_player_stats', getPlayerStatsRpc);
// RPC: avvia ricerca matchmaking
initializer.registerRpc('start_matchmaking', startMatchmakingRpc);
// RPC: acquisto item
initializer.registerRpc('purchase_item', purchaseItemRpc);
// === MATCH HANDLER ===
initializer.registerMatch('game_match', {
matchInit: matchInit,
matchJoinAttempt: matchJoinAttempt,
matchJoin: matchJoin,
matchLeave: matchLeave,
matchLoop: matchLoop,
matchSignal: matchSignal,
matchTerminate: matchTerminate
});
}
// RPC: acquisto item con verifica wallet
function purchaseItemRpc(ctx: nkruntime.Context, logger: nkruntime.Logger,
nk: nkruntime.Nakama, payload: string): string {
interface PurchaseRequest { item_id: string; quantity: number; }
const req: PurchaseRequest = JSON.parse(payload);
// Leggi catalogo item dallo storage
const storageObjects = nk.storageRead([{
collection: 'catalog',
key: req.item_id,
userId: ''
}]);
if (storageObjects.length === 0) {
throw new Error(`Item ${req.item_id} not found in catalog`);
}
interface CatalogItem { price_coins: number; type: string; }
const item = JSON.parse(storageObjects[0].value) as CatalogItem;
const totalCost = item.price_coins * req.quantity;
// Transazione atomica: scala wallet e concedi item
try {
nk.walletUpdate(ctx.userId!, { coins: -totalCost }, {
reason: `purchase_${req.item_id}`,
quantity: req.quantity
}, false); // false = errore se saldo insufficiente
// Salva item nell'inventario
nk.storageWrite([{
collection: 'inventory',
key: req.item_id,
userId: ctx.userId!,
value: JSON.stringify({
item_id: req.item_id,
quantity: req.quantity,
purchased_at: Date.now()
}),
permissionRead: 1, // owner only
permissionWrite: 0 // server only
}]);
return JSON.stringify({ success: true, new_quantity: req.quantity });
} catch (e) {
throw new Error(`Purchase failed: ${e}`);
}
}
3. Yetkili Eşleşme: Sunucu Tarafı Oyun Döngüsü
Rekabetçi çok oyunculu oyunlar için Nakama'nın en güçlü özelliği veyetkili eşleşme: Tamamen sunucu tarafında çalışan, istemci girişlerini işleyen ve durumu hesaplayan bir oyun döngüsü Oyunun yetkilisi. Yerel Nakama tarafı müşteri tahmini yoktur ancak bunu uygulayabilirsiniz istemcinizde ve bunu sunucu durumuyla uzlaştırın.
// runtime/match_handler.ts - Authoritative match game loop
interface GameState {
players: Map<string, PlayerState>;
phase: 'waiting' | 'playing' | 'ending';
tick: number;
start_time: number;
config: MatchConfig;
}
interface PlayerState {
user_id: string;
position: { x: number; y: number };
health: number;
score: number;
alive: boolean;
}
// Inizializzazione match: configura stato iniziale
function matchInit(ctx: nkruntime.Context, logger: nkruntime.Logger,
nk: nkruntime.Nakama, params: { [key: string]: string }) {
const initialState: GameState = {
players: new Map(),
phase: 'waiting',
tick: 0,
start_time: 0,
config: {
max_players: parseInt(params['max_players'] ?? '8'),
tick_rate: parseInt(params['tick_rate'] ?? '20'), // 20 tick/s
match_duration_sec: parseInt(params['duration'] ?? '300')
}
};
return {
state: initialState,
tickRate: initialState.config.tick_rate,
label: JSON.stringify({ mode: params['mode'] ?? 'deathmatch' })
};
}
// Game loop principale: eseguito ad ogni tick (20 volte/secondo)
function matchLoop(ctx: nkruntime.Context, logger: nkruntime.Logger,
nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher,
tick: number, state: GameState,
messages: nkruntime.MatchMessage[]): GameState | null {
state.tick = tick;
// Processa tutti i messaggi dai client in questo tick
for (const message of messages) {
const userId = message.sender.userId;
const opCode = message.opCode;
const data = JSON.parse(message.data) as Record<string, number>;
switch (opCode) {
case 1: // Movimento
const player = state.players.get(userId);
if (player?.alive) {
// Validazione server-side: max velocità
const dx = Math.min(Math.abs(data.dx), 5) * Math.sign(data.dx);
const dy = Math.min(Math.abs(data.dy), 5) * Math.sign(data.dy);
player.position.x += dx;
player.position.y += dy;
// Clamp dentro i bordi della mappa
player.position.x = Math.max(0, Math.min(100, player.position.x));
player.position.y = Math.max(0, Math.min(100, player.position.y));
}
break;
case 2: // Attacco
if (state.phase === 'playing') {
processAttack(state, userId, data);
}
break;
}
}
// Aggiorna fase di gioco
if (state.phase === 'waiting') {
const readyCount = Array.from(state.players.values())
.filter(p => p.alive).length;
if (readyCount >= 2) {
state.phase = 'playing';
state.start_time = Date.now();
}
} else if (state.phase === 'playing') {
const elapsed = (Date.now() - state.start_time) / 1000;
const aliveCount = Array.from(state.players.values())
.filter(p => p.alive).length;
if (elapsed >= state.config.match_duration_sec || aliveCount <= 1) {
state.phase = 'ending';
// Broadcast risultati finali
const results = buildResults(state);
dispatcher.broadcastMessage(99, JSON.stringify(results), null, null, true);
return null; // Termina il match
}
}
// Broadcast stato a tutti i giocatori ogni 3 tick (67ms) per ridurre bandwidth
if (tick % 3 === 0) {
const snapshot = buildStateSnapshot(state);
dispatcher.broadcastMessage(0, JSON.stringify(snapshot), null, null, false);
}
return state;
}
function processAttack(state: GameState, attackerId: string,
data: Record<string, number>): void {
const attacker = state.players.get(attackerId);
if (!attacker?.alive) return;
// Trova target nel raggio di attacco (5 unita)
for (const [id, player] of state.players.entries()) {
if (id === attackerId || !player.alive) continue;
const dx = player.position.x - attacker.position.x;
const dy = player.position.y - attacker.position.y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance <= 5) {
const damage = data.damage ?? 10;
player.health -= damage;
if (player.health <= 0) {
player.alive = false;
player.health = 0;
attacker.score += 100; // Uccisione
}
}
}
}
4. Nakama'nın Kubernetes'te Dağıtımı
Nakama, Kubernetes'te yatay olarak ölçeklenecek şekilde tasarlanmıştır. CockroachDB'yi veritabanı olarak kullanın düğümler arasındaki veri tutarlılığı için dağıtılmış (PostgreSQL ile uyumlu) ve Redis önbelleğe alma ve gerçek zamanlı düğümler arası iletişim.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nakama
namespace: game-backend
spec:
replicas: 3 # Scaling orizzontale: ogni replica gestisce match diversi
selector:
matchLabels:
app: nakama
template:
metadata:
labels:
app: nakama
spec:
containers:
- name: nakama
image: heroiclabs/nakama:3.22.0
ports:
- containerPort: 7349 # gRPC
- containerPort: 7350 # HTTP API
- containerPort: 7351 # Console
env:
- name: NAKAMA_DATABASE_ADDRESS
valueFrom:
secretKeyRef:
name: nakama-secrets
key: db-address
- name: NAKAMA_RUNTIME_PATH
value: "/nakama/data/modules"
args:
- "--database.address=$(NAKAMA_DATABASE_ADDRESS)"
- "--cache.address=redis:6379"
- "--runtime.path=$(NAKAMA_RUNTIME_PATH)"
- "--session.token_expiry_sec=86400"
- "--socket.server_key=nakama-server-key-production"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
volumeMounts:
- name: runtime-modules
mountPath: /nakama/data/modules
volumes:
- name: runtime-modules
configMap:
name: nakama-runtime-js
---
apiVersion: v1
kind: Service
metadata:
name: nakama
namespace: game-backend
spec:
type: LoadBalancer # O NodePort + Ingress nginx
selector:
app: nakama
ports:
- name: grpc
port: 7349
targetPort: 7349
- name: http
port: 7350
targetPort: 7350
---
# HPA: scala da 3 a 20 repliche in base a CPU
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nakama-hpa
namespace: game-backend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nakama
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
5. Açık Eşleştirme: Google'ın Çöpçatanlık Çerçevesi
Open Match, Google tarafından oyunlara yönelik ölçeklenebilir ve modüler bir eşleştirme sistemi olarak geliştirildi Milyonlarca oyuncuyla. Mimarisi iletişim kuran üç ayrı bileşene dayanmaktadır. gRPC aracılığıyla yalnızca sizi ilgilendiren kısmı (eşleştirme mantığı) özelleştirmenize olanak tanır tüm altyapıyı yeniden yazmak zorunda kalmadan.
Match bileşenlerini aç
| Bileşen | Sorumluluk | Özelleştirilebilir |
|---|---|---|
| Başlangıç aşaması | Müşterilerden eşleştirme isteklerini alır (bilet oluşturma) | Hayır (çekirdek) |
| Arka uçlar | Direktörün getir-eşleştir-atama döngüsünü yönetir | Hayır (çekirdek) |
| Eşleştirme İşlevi | Eşleştirme algoritması: biletleri maçlara göre gruplandırır | Evet - UYGULANMALIDIR |
| Değerlendirici | Önerilen maçlar arasındaki anlaşmazlıkları çözer (birden fazla maçta tek bilet) | Evet - isteğe bağlı |
| Müdür | Orkestratör: Eşleştirme işlevini çağırın, sunucuyu atayın, bilgilendirin | Evet - UYGULANMALIDIR |
// Director: orchestratore del matchmaking (Go)
package main
import (
"context"
"fmt"
"time"
ompb "open-match.dev/open-match/pkg/pb"
"google.golang.org/grpc"
)
func director(ctx context.Context, be ompb.BackendServiceClient) {
profile := &ompb.MatchProfile{
Name: "deathmatch-8v8",
Pools: []*ompb.Pool{
{
Name: "all-players",
DoubleRangeFilters: []*ompb.DoubleRangeFilter{
{
DoubleArg: "mmr",
Min: 0,
Max: 3000,
},
},
},
},
}
for {
select {
case <-ctx.Done():
return
default:
// Fetcha match proposti dalla match function
stream, err := be.FetchMatches(ctx, &ompb.FetchMatchesRequest{
Config: &ompb.FunctionConfig{
Host: "matchfunction",
Port: 50502,
Type: ompb.FunctionConfig_GRPC,
},
Profile: profile,
})
if err != nil {
fmt.Printf("FetchMatches error: %v\n", err)
time.Sleep(5 * time.Second)
continue
}
for {
resp, err := stream.Recv()
if err != nil {
break
}
// Assegna server di gioco al match
conn, err := assignGameServer(resp.Match)
if err != nil {
fmt.Printf("GameServer assignment error: %v\n", err)
continue
}
// Notifica i ticket del loro assignment
ids := make([]string, len(resp.Match.Tickets))
for i, t := range resp.Match.Tickets {
ids[i] = t.Id
}
be.AssignTickets(ctx, &ompb.AssignTicketsRequest{
Assignments: []*ompb.AssignmentGroup{{
TicketIds: ids,
Assignment: &ompb.Assignment{
Connection: conn, // "game-server-eu-west:7777"
},
}},
})
}
time.Sleep(1 * time.Second) // Polling ogni secondo
}
}
}
// Match Function: logica di matching con Glicko-2
// Raggruppa ticket in match di 8 giocatori con rating simile
func matchFunction(ctx context.Context, req *mmfpb.RunRequest) (
*mmfpb.RunResponse, error) {
tickets := req.Pooled_tickets["all-players"]
var matches []*ompb.Match
// Ordina per MMR per trovare gruppi omogenei
sort.Slice(tickets, func(i, j int) bool {
mmrI := tickets[i].SearchFields.DoubleArgs["mmr"]
mmrJ := tickets[j].SearchFields.DoubleArgs["mmr"]
return mmrI < mmrJ
})
// Raggruppa in match da 8, tolleranza MMR +-200
matchSize := 8
for i := 0; i+matchSize-1 < len(tickets); i += matchSize {
group := tickets[i : i+matchSize]
// Verifica spread MMR accettabile
minMMR := group[0].SearchFields.DoubleArgs["mmr"]
maxMMR := group[matchSize-1].SearchFields.DoubleArgs["mmr"]
if maxMMR-minMMR > 200 {
continue // Spread troppo alto, skip
}
matches = append(matches, &ompb.Match{
MatchId: fmt.Sprintf("dm8v8-%d", time.Now().UnixNano()),
MatchProfile: req.GetProfile().GetName(),
MatchFunction: "glicko2-deathmatch",
Tickets: group,
})
}
return &mmfpb.RunResponse{Proposals: matches}, nil
}
6. Nakama + Açık Maç Entegrasyonu
Nakama ve Open Match birbirlerini mükemmel bir şekilde tamamlıyor: Nakama kimlik doğrulamayı, sosyal depolamayı yönetiyor, varlık ve sohbet, Open Match ise ölçeklenebilir eşleştirmeyi yönetiyor. Entegrasyon gerçekleşir Açık Maç bileti oluşturan bir Nakama RPC ve Nakama'yı bilgilendiren bir Açık Maç web kancası aracılığıyla sunucu ataması.
// Nakama RPC: crea ticket di matchmaking su Open Match
function startMatchmakingRpc(ctx: nkruntime.Context,
logger: nkruntime.Logger, nk: nkruntime.Nakama,
payload: string): string {
interface MatchmakingRequest { mode: string; region: string; }
const req: MatchmakingRequest = JSON.parse(payload);
// Recupera MMR del giocatore dallo storage
const statsObjs = nk.storageRead([{
collection: 'player_stats',
key: 'rating',
userId: ctx.userId!
}]);
const mmr = statsObjs.length > 0
? (JSON.parse(statsObjs[0].value) as { mmr: number }).mmr
: 1000; // MMR default per nuovi giocatori
// Chiama Open Match Frontend via HTTP
const response = nk.httpRequest(
'http://open-match-frontend:51504/v1/tickets',
'POST',
{ 'Content-Type': 'application/json' },
JSON.stringify({
search_fields: {
double_args: {
mmr: mmr
},
string_args: {
mode: req.mode,
region: req.region
}
},
extensions: {
player_id: ctx.userId!,
nakama_session: ctx.sessionId!
}
})
);
if (response.code !== 200) {
throw new Error(`Open Match error: ${response.code}`);
}
interface OMTicket { id: string; }
const ticket = JSON.parse(response.body) as OMTicket;
// Salva ticket ID per polling status
nk.storageWrite([{
collection: 'matchmaking',
key: 'current_ticket',
userId: ctx.userId!,
value: JSON.stringify({
ticket_id: ticket.id,
mode: req.mode,
created_at: Date.now()
}),
permissionRead: 1,
permissionWrite: 0
}]);
return JSON.stringify({
ticket_id: ticket.id,
status: 'searching'
});
}
7. Açık Kaynak ve Yönetilen: Ne Zaman Neyi Seçmeli
Açık kaynak yığınları (Nakama + Open Match) ve yönetilen çözümler (PlayFab, GameLift, Lootlocker) çeşitli faktörlere bağlıdır. Evrensel bir cevap yok.
Detaylı Karşılaştırma
| Kriter | Açık Kaynak (Nakama) | Yönetilen (PlayFab) |
|---|---|---|
| Maliyet (1 milyon MAU) | ~5.000$/ay altyapı | ~15.000$/ay lisans + altyapı |
| Satıcıya Kilitlenme | Yok (Apache 2.0) | Yüksek (Azure/AWS verileri) |
| Özelleştirme | Toplam (kaynak kodu mevcut) | Açığa çıkan API'lerle sınırlıdır |
| Pazara Çıkış Zamanı | Haftalar (kurulum + öğrenme) | Günler (sihirbaz + şablon) |
| Otomatik ölçeklendirme | Kılavuz (Kubernetes HPA) | Otomatik ve yönetilen |
| Uyumluluk (GDPR) | Toplam veri kontrolü | Sağlayıcıya bağlıdır |
| Topluluk/Destek | Açık kaynak topluluğu, Discord | Garantili SLA, kurumsal destek |
Seçim Kılavuzu
- Aşağıdaki durumlarda Açık Kaynak'ı seçin: Yetkin bir DevOps ekibiniz var, 500 binden fazla MAU bekliyorsunuz, sıkı uyumluluk gereksinimleriniz (GDPR, oyun düzenlemesi) var veya kapsamlı özelleştirmeler istiyorsunuz eşleştirme/depolama mantığı.
- Aşağıdaki durumlarda Yönetilen'i seçin: Küçük bir ekibe sahip bağımsız bir stüdyosunuz, prototip yapmak istiyorsunuz hızlı veya hacimler konusunda kesinlik kazanmadan erken erişim aşamasındasınız.
- Hibrit strateji: Lansman öncesi riski azaltmayı başararak başlayın, geçiş yapın Oyunu ve ciltleri doğruladıktan sonra açık kaynağa geçin.
Sonuçlar
Nakama ve Open Match, 2025'teki açık kaynaklı oyun arka uçlarının son durumunu temsil ediyor. Nakama, sosyal özelliklerin ve çok oyunculu oyunların karmaşıklığını zarif bir API ile ele alıyor sınırsız özelleştirmeye izin veren bir çalışma zamanı kanca sistemi. Açık Maç getiriyor açık kaynak dünyası, uygulama imkanı ile ölçeklenebilir ve modüler bir eşleştirme herhangi bir eşleştirme algoritması.
Bu araçlarla başarının anahtarı, Kubernetes'in derinlemesine anlaşılmasıdır. dağıtım ve TypeScript veya Go'da sağlam çalışma zamanı mantığı yazma yeteneği. Zamana yatırım yapın Bu bilgi birikimiyle: Yönetilen çözümlerle karşılaştırıldığında tasarruflar önemli düzeydedir ve Oyun ölçeklendiğinde elde ettiğiniz mimari özgürlük paha biçilemez.
Oyun Arka Uç Serisindeki Sonraki Adımlar
- Önceki makale: Hile Önleme Mimarisi: Sunucu Yetkilisi
- Sonraki makale: LiveOps: Etkinlik Sistemi ve Özellik İşareti
- Daha fazla bilgi: GameLift ve Agones ile Oyun Sunucusu Düzenlemesi







