FIT ČVUT

Adam Vesecký

NI-APH
Přednáška 11

Multiplayer

Architektura Multiplayeru

Multiplayer

Umožňuje vícero hráčům interagovat ve stejném herním světě a ve stejném čase.
Velmi komplexní záležitost, která má zásadní dopad na celou architekturu hry.

Kategorie multiplayeru

  • single-screen multiplayer
  • split-screen multiplayer
  • network multiplayer
  • MMOG
  • Cloud Gaming

Historie multiplayeru

Lokální multiplayer

  • Spacewar! (1962)

Síťový multiplayer

  • Empire (1973)

Lokální síťový multiplayer

  • Doom (1993)

Online multiplayer

  • Quake (1996)
  • Unreal (1998)

Online-only multiplayer

  • World of Tanks (2010)
  • Diablo 3 (2012)

MMO

  • Ultima Online (1997)
  • World of Warcraft (2004)

Dříve a dnes

Multiplayer v roce 1977

Multiplayer v roce 2004

Dříve a dnes

Multiplayer v roce 2023

O co se jedná

  • hlavní cíl - synchronizovat několik univerz
  • všichni klienti musí dosáhnout jistého stavu synchronicity
  • atributy ovlivňující kvalitu gameplaye: rychlost, latence, stabilita, ztrátovost paketů
  • neexistuje žádný reálný předobraz pro tento typ problému
  • multi-player hru do single-playeru zkonvertujeme snadno
  • opačně to jde dost těžko

Metody

  • přenos kompletního herního stavu všem klientům - pro jednoduché hry
  • přenos podmnožiny herního stavu, ze kterého lze zkonstruovat zbytek

Topologie

  • peer-to-peer (už se nepoužívá)
  • client-server

Peer-to-peer architektura

  • zařízení si vzájemně vyměňují data v úplném grafu
  • použito ve hře Doom, prvních Command & Conquer hrách, Age of Empires, Starcraft
  • máme-li n  peerů, každý musí mít n-1  spojení -> O(n^2)  celkem
  • metody: single master, partial authority, full replication

Peer-to-peer hry

Doom (1993)

  • 14.4 kbps PPP nebo 28.8 kbps SLIP
  • v každém cyklu si zařízení vymění vstupy od hráčů
  • je potřeba čekat na vstup od hráče s nejvyšším lagem
  • každých 30ms se vstup od hráče sampluje do příkazu tic
  • jakmile se získá tic command od všech hráčů, hra aktualizuje svůj stav

Age of Empires (1997)

  • pro bitvu 8 hráčů umožňuje až 400 jednotek
  • používal Turn Timer - fronta pro příkazy
  • AoE synchronizuje příkazy od každého hráče
  • všechny příkazy za 200ms se uloží do bufferu
  • jakmile uplyne 200ms, všechny příkazy pro hráčův tah se odešlou ostatním

Client-Server architektura

  • n  zařízení, n-1  spojení
  • server musí zpracovat (n-1)\times  více zpráv za sekundu
  • server se rychle stane bottleneckem
  • Dedikovaný server - nic nerenderuje, pouze komunikuje a běží na něm hra
  • Listen server - server je aktivní účastník hry

Příklad: Quake

  • první hra, která použila mechanismus částečné spolehlivosti
  • výstupy jsou zachyceny prediktivní vrstvou
  • server běží na 20 FPS, a klient na 60 FPS

Command

  • základní jednotka komunikace
  • používán pro aktualizaci pozice hráče, směru, HP,...
  • důležité příkazy (s dopadem na herní stav) se potvrzovaly

NetChannel header

Transport

TCP vs UDP

  • na rozdíl od aplikací mají hry dva hlavní typy zpráv
  • akce ovlivňující gameplay musí být potvrzeny
  • dynamická/streamovací data nepotřebují potvrzení
  • zprávy mohou mít různou prioritu
  • obvykle se používá vlastní protokol jako nadstavba UDP
Použití TCP je to nejhorší, co může člověk při implementaci multiplayer hry udělat.Glenn Fielder, 2008

Architektura multiplayer enginu

  • na serveru běží hra a klienti pouze zpracovávají vstupy a zobrazují stav hry

Problémy

  • každý hráč má pouze částečně konzistentní informaci o herním stavu
  • v některých případech může server nesouhlasit se stavem klienta
  • mezi provedením akce a zobrazením jejího dopadu je zpoždění

Příklad: Message Header

  • SEQ number - sekvenční číslo zprávy
  • ACK number - číslo jiné zprávy, které je potřeba potvrdit
  • ACK bit array - potvrzující flag pro předchozích 32 zpráv
  • Type - typ zprávy (update, disconnect, command,...)
  • ActionID - identifikátor akce

Typy zpráv

Stream

  • nepotřebuje potvrzení, obsahuje kolekci spojitých hodnot (např. pozice, rotace)

Snapshot

  • kompletní herní stav, zasílá se buď na vyžádání či v pevných intervalech

Event

  • zpráva, která ovlivňuje herní stav, musí být potvrzena
  • příklady: UNIT_CREATED, UNIT_DESTROYED, BUILDING_COMPLETED

Akce

  • zpráva s vysokou prioritou (hráčův vstup)

Generická zpráva

  • např. vzdálené volání procedur (přehraj zvuk, resetuj animaci)

Connection

  • zprávy pro discovery, handshake, registraci, disconnect, atd.

Beacon

  • pravidelné zprávy informující server, že je spojení stále živé

Příklad: Goat Attacks

Serializace

  • komplexní herní svět může mít stovky pohybujících se objektů
  • potřebujeme odstranit vše, co není potřeba posílat po síti
  • zprávy by měly být co nejblíže MTU (~1500B)

Příklad bez optimalizace

  • RTS bitva
  • 5 hráčů
  • 500 pohybujících se jednotek
  • každá jednotka má 20 atributů velikosti 32-bitů -> 80 B na jednotku
  • server pošle 30 zpráv za sekundu
  • velikost headeru je 42B (IP + UDP + networking header)
  • potřebný bandwidth pro server: 30 \cdot 5 \cdot (42 + 500 \cdot 80) \cdot 8 \approx 52  Mbps

Serializace - binární footprint

  • serializujeme všechno (ne úplně robustní řešení)

struct Mage {

    int health;

    int mana;

}

 

void Serialize(const Mage* mage) {

    SendMessage(reinterpret_cast<const char*>(mage), sizeof(Mage));

}

 

void Deserialize(const Mage* output) {

    ReceiveMessage(reinterpret_cast<char*>(output), sizeof(Mage));

}

Health = 10, Mana = 14; Little Endian

Serializace - streamování

  • streamy nám umožní zvolit si, jaké atributy serializovat a jak
  • dobré řešení pro kolekce

 struct Mage : public Streamable {

    vector<Item*> items;

    int health;

    int mana;  

  

    void SaveToStream(NetWriter* writer) {

        writer->WriteDWord(health);

        writer->WriteDWord(mana);

        writer->WriteDWord(items.size());

        for(auto item : items) item->SaveToStream(writer);

    }

  

    void LoadFromStream(NetReader* reader) {

        health = reader->ReadDWord();

        mana = reader->ReadDWord();

        int itemsCount = reader->ReadDWord();

        for(int i=0; i<itemsCount; i++) {

            items.push_back(new Item(reader));

        }

    }

}

Komprese

Komprese bitů

  • použijeme co nejméně bitů na každou proměnnou
  • můžeme pracovat s omezeným rozsahem a přesností

Enkódování entropie

  • komprimujeme data na základě četnosti výskytu
  • příklad: pokud je rotace skoro vždy 0, budeme ji posílat jen pokud bude mít jinou hodnotu

Komprese

Komprese atributů

  • serializujeme pouze změněné atributy
  • každý objekt má bitové pole, které indikuje, které atributy jsou obsaženy ve zprávě

Komprese celé zprávy

  • Huffmanovo kódování, LZ4,...

Komprese

Delta zprávy

  • pokud se číslo příliš nezmění, nezmění se příliš ani jeho bity
  • mezi dvěma framy se změní pouze několik málo bitů (pozice, rotace)
  • uložíme pouze rozdíly pomocí XOR operace
  • velmi efektivní, ale citlivé na ztrátovost paketů
  • můžeme uložit 2/3 atributů jako rozdíl a 1/3 jako plná data - budeme potřebovat pouze 3 po sobě jdoucí pakety pro reprodukci kompletního stavu

Replikace

  • přenos stavu objektu z jednoho zařízení na druhý
  • každý objekt musí mít svůj identifikátor (network ID)
  • zpráva obsahuje typ objektu a všechny parametry nutné k jeho konstrukci

 switch(actionType) {

    case OBJECT_CREATED:

      int objectType = reader->ReadDWord();

      auto factory = creatorManager->FindObjectFactory(objectType);

      auto newInstance = factory->CreateInstance(reader); // parse parameters

      objects.push_back(newInstance);

    break;

    case OBJECT_DELETED:

      int objectId = reader->ReadDWord();

      sceneManager->removeObjectById(objectId);

    ...

  }

Spolehlivost

  • pakety se mohou ztratit
  • server bude posílat zprávy opakovaně, dokud klient nepošle potvrzení

Řazení

  • pakety mohou přijít zpřeházené
  • klient by neměl aplikovat zprávu na herní model, dokud neaplikuje všechny předchozí zprávy

Latence

Latence

  • čas, který uplyne mezi provedením akce a pozorováním jejího následku
  • např. klik myší mající za cíl přesun jednotky po mapě

Akceptovatelná latence

  • FPS: 16-60 ms
  • RTS: < 250 ms

Latence mimo síťový přenos

  • samplování vstupu (~2 ms)
  • vykreslování (< 16 ms)
  • dekódování zprávy (~2-16 ms)

Síťová latence

  • procesní zpoždění (routování)
  • zpoždění na frontách (router může zpracovat pouze omezený počet paketů najednou)
  • zpoždění přenosem (informace nemůže cestovat rychleji než světlo)

Latence - příklad

  • oba klienti mají stejnou latenci

Latence - příklad

  • klient B má vyšší latenci

Synchronizace

Interpolace

  • klient může běžet na 60 FPS, server obvykle posílá aktualizace na 10-30 FPS
  • Interpolace: jakmile klient obdrží nový stav, bude k němu interpolovat
  • Problém: objekt může změnit hodnotu okamžitě (např. teleport)

Interpolační rezerva

  • klient renderuje stav, který je 2 framy starý
  • hladší interpolace, ale vytváří umělé zpoždění

Extrapolace

  • bez interpolační rezervy se klient pokouší extrapolovat
  • výsledkem je trhavá animace

Deterministická predikce

  • interpolační rezerva je přesná ale zpožděná
  • klient je vždy alespoň 1/2 RTT za skutečným stavem
  • některé atributy je možno zpracovávat přímo na straně klienta (např. pozice kamery v FPS)

Predikce

  • klient predikuje budoucí hodnotu tím, že spustí ten samý kód jako server
  • aby mohl extrapolovat o 1/2 RTT, musí klient odhadnout RTT

Nedeterministická predikce

  • nedeterministické hodnoty jsou špatně předvídatelné (steering behaviors, pohyb ostatních hráčů,...)
  • řešení: zkusit uhádnout výsledek a opravit ho, jakmile dorazí update
  • Dead reckoning - predikce chování entity za předpokladu, že se bude chovat stejně jako doposud

Simulace na serveru

Špatná predikce na klientovi

Server-side rewind

  • metodika zpracování instantních akcí, které ovlivňují gameplay (např. zásahy)
  • bere v potaz dilataci času a jiné nepřesnosti
  • umožňuje serveru změnit stav, který již byl potvrzen

Řešení Source enginu

  • vrátit stav na serveru do okamžiku, kdy hráč provedl akci

Špatně

Správně

Příklad: Server-side rewind

  • Klient B má 3x vyšší latenci než klient A
  • Klient B provedl změnu před klientem A

Řešení problému s latencí - shrnutí

Dilatace času

  • hodnoty se uměle zpozdí několik framů a klienti mezi nimi interpolují

Deterministická predikce

  • spustí simulaci, maskuje latenci a udržuje stav klienta synchronní

Dead reckoning

  • nedeterministická predikce
  • klient použije několik posledních stavů k extrapolaci budoucích stavů

Server-side rewind

  • server se vrátí několik framů zpět a otestuje akci z pohledu klienta
Je lepší se trochu mýlit včas než mít pravdu a pozdě.

Příklad: Source engine

  • server simuluje svět v 30-60 FPS a posílá 20 snapshotů za sekundu, používaje interpolační rezervu 100 ms
  • klient sampluje vstup v 30-60 FPS a bufferuje snapshoty pro 100 ms
  • server používá dilataci času a rewind

Bezpečnost

Obvyklé hrozby

  • packet sniffing, man-in-the-middle
  • ghosting - šmírování hráče skrze vícero připojení

Validace vstupu

  • znemožní hráči provést akci, která není validní
  • např. pouze klient, na kterém hraje hráč A, může poslat akci, která ovládá hráče A

Cheat detection software

  • aktivně monitoruje integritu hry
  • podvodný SW se může attachnout na herní proces, přepisovat paměť či modifikovat soubory
  • map hacking - např. odstranění fog-of-war
  • bot cheat - bot, který hraje za hráče nebo mu různě asistuje
    • příklad: dummy levelling, aimbot
  • Valve Anti-Cheat - dostupný pro hry, které mají Steamworks SDK
    • udržuje list zabanovaných uživatelů a scanuje běžící hry
  • Easy AntiCheat - znemožňuje cheatování na technické úrovni

Knihovny

ReplicaNet

  • knihovna pro sdílení objektů

RakNet

NetStalker

Netcode for GameObjects (NGO)

  • networking knihovna pro Unity

Co jsme si řekli

  • Jaký je rozdíl mezi P2P a clinet-server architekturou
  • Jaké problémy se v multiplayeru řeší
  • Jaké typy zpráv se používají v multiplayerových hrách
  • Co je to replikace
  • Jak v multiplayeru funguje interpolace
  • Jak hry řeší latenci

Hláška na závěr

Nedosažitelná výška musí být VIDITELNĚ VÝŠ než maximální výskok hráče.John Wiley