NEON CITYmaking of
02 / 07

Architektura

Jedna symulacja, dwa miejsca wykonania

Ta sama deterministyczna symulacja liczy grę na serwerze i – z wyprzedzeniem – w przeglądarce. Serwer ma ostatnie słowo, klient przewiduje, a w trybie solo serwer działa lokalnie w karcie.

2.1 Komponenty

Klient to aplikacja three.js: renderuje miasto, HUD i broń, zbiera wejście gracza i przewiduje skutki własnych ruchów. Nie podejmuje jednak decyzji o trafieniach, obrażeniach ani o tym, co zrobi horda – to zadanie symulacji.

Symulacja (pakiet sim) jest deterministyczna: ten sam stan i te same wejścia dają co do bitu ten sam wynik. Dzięki temu jeden kod działa w trzech rolach – na serwerze w chmurze, w przeglądarce jako predykcja i w trybie solo jako LocalServer w karcie, bez sieci.

Backend stoi na Cloudflare: statyczny klient na Pages, API na Workers, pokoje co-op i ich checkpointy w Durable Objects, kafle i gotowe areny w R2. Pakiet protocol pilnuje, żeby klient i serwer mówiły tym samym językiem.

Diagram komponentów: w przeglądarce klient three.js i LocalServer, pakiety wspólne sim i protocol, w Cloudflare Pages, Workers, Durable Objects z pokojami co-op i R2 z kaflami i arenami; generator aren zapisuje do R2

2.2 Mecz co-op z predykcją

Diagram sekwencji meczu co-op: klient wysyła wersję protokołu i defsHash, pokój odpowiada welcome albo odmową; potem w każdym ticku klient przewiduje, wysyła input, serwer liczy tick i odsyła snapshot, klient wykonuje rekonsyliację

Zanim gracz wejdzie do pokoju, klient i serwer porównują wersję protokołu i defsHash – skrót wszystkich definicji rozgrywki (broni, przeciwników, tramwajów). Niezgodna para jest odrzucana jeszcze przed powitaniem. Lepszy czytelny komunikat niż mecz, w którym dwie strony liczą inne obrażenia.

W trakcie gry klient nie czeka na serwer: od razu przewiduje własny ruch tym samym kodem symulacji. Gdy przychodzi snapshot, porównuje go z przewidywaniem, cofa się do potwierdzonego stanu i odtwarza niepotwierdzone wejścia. Pozostałych graczy i boty interpoluje.

Przy opóźnieniu 150 ms z wahaniami 30 ms różnica między predykcją a serwerem w testach tramwajów wynosiła 0 m; pozycje zdalnych encji – do 1 mm.

2.3 Areny z OpenStreetMap

Każda arena to okrąg o promieniu 600 m wokół rynku prawdziwego miasta. Generator czyta kafle z danymi OpenStreetMap i zapisuje trzy pliki: JSON z opisem areny, PRE z danymi wstępnie policzonymi i GEO z geometrią i colliderami.

Generator ma wersję GEN – skrót reguł, które wpływają na wynik. Zmiana reguły zmienia GEN, a pliki trafiają do nowego prefiksu w R2, więc stara i nowa wersja nigdy się nie mieszają. Testy mutacyjne sprawdzają w obie strony: zmiana reguły musi zmienić wersję, a zmiana, która na wynik nie wpływa – nie.

Generacja jest deterministyczna. W ostatniej dostawie 20 z 20 aren miało identyczne sumy SHA-256 we wszystkich trzech plikach w dwóch niezależnych przebiegach. Największa arena po kompresji zajmuje 853 KB – poniżej limitu 1 MB.

Diagram pipeline’u aren: OpenStreetMap, kafle wektorowe, generator aren r600, pliki JSON, PRE i GEO, wersja GEN i dwa przebiegi z identycznymi bajtami, zapis do R2 pod prefiksem wersji, wczytanie przez klienta i serwer
Tramwaje w arenach r600 – dane z generatora
Arena (seed 1)Trasy spełniające warunkiTramwajePrzystanki na wybranych trasach
Wrocław, Rynek4633
Łódź, Plac Wolności2435
Kraków, Rynek Główny2034
Warszawa, Stare Miasto430
Skierniewice, Rynek000

2.4 Tramwaj jako funkcja czasu

Tramwaj jest w grze dużym, ruchomym obiektem, który potrafi zepchnąć gracza i zadać obrażenia. Wysyłanie jego pozycji w każdym snapshocie byłoby proste – i kosztowne. Zamiast tego położenie tramwaju jest czystą funkcją danych areny, jej seeda i numeru ticku. Serwer i klient importują tę samą implementację, więc obie strony wiedzą, gdzie jest tramwaj, bez jednego bajtu w sieci.

Rozkład nie zależy od graczy, jakości grafiki ani zegara ściennego. Snapshot i checkpoint nie zawierają stanu tramwajów – stan ma tylko gracz (np. czas do kolejnego trafienia), więc po wznowieniu meczu tramwaj jest dokładnie tam, gdzie powinien.

Kraków nocą, telefon – postój: tramwaj stoi przy peronie, drzwi jeszcze zamknięte
01Postój
Kraków nocą, telefon – otwarcie: drzwi otwarte po stronie peronu
02Otwarcie
Kraków nocą, telefon – zamknięcie: drzwi zamknięte po 8 s postoju
03Zamknięcie
Kraków nocą, telefon – odjazd: tramwaj rusza i oddala się
04Odjazd

Korytarz ostrzegawczy (D5) to świetlny pas na jezdni, rysowany z przyszłych pozycji tramwaju na najbliższe 3 s – też z tej samej funkcji czasu. Pierwsza wersja przeszła testy, ale z góry była niewidoczna; jak to wykryto i naprawiono, opisuje studium TRAM-1, a porównanie przed/po jest w sekcji 6.

Kontakt z tramwajem: wypchnięcie zawsze, obrażenia 5 HP tylko gdy tramwaj jedzie szybciej niż 0,5 m/s i najwyżej raz na przejazd. Postój przy peronie jest bezpieczny.

Łódź nocą z góry: przed tramwajem na jezdni świeci pomarańczowy korytarz ostrzegawczy
D5 nocą – korytarz z pozycji na 3 s naprzód. Mapa: © OpenStreetMap contributors (ODbL).