Architektura trybu wieloosobowego
Tryb wieloosobowy En Parlant~ wykorzystuje serwer przekaźnikowy WebSocket do łączenia dwóch graczy w czasie rzeczywistym. Nie ma tu połączeń peer-to-peer — cała komunikacja przechodzi przez serwer przekaźnikowy. Dzięki temu warstwa sieciowa jest prosta i działa niezawodnie za firewallami i NAT-ami.
Przegląd architektury
Dział zatytułowany „Przegląd architektury”Player A (host) Relay Server Player B (joiner) | | | |--- create_game(name) ----------->| | |<-- game_created(code) -----------| | | |<--- join_game(code, name) -----| |<-- game_joined(name) ------------|--- game_joined(name) -------->| | | | |--- game_move(uci, times) ------->|--- game_move(uci, times) --->| |<-- game_move(uci, times) --------|<-- game_move(uci, times) ----| | | | |--- heartbeat ------------------>|--- peer_heartbeat ----------->|Serwer przekaźnikowy to cienka warstwa przekierowująca. Przechowuje pokoje w pamięci, kieruje zdarzenia między dwoma graczami w każdym pokoju i obsługuje czyszczenie zasobów. Na serwerze nie ma żadnej logiki gry ani walidacji ruchów — autorytatywni są klienci.
Stos technologiczny
Dział zatytułowany „Stos technologiczny”- Frontend (klient): socket.io-client — standardowy klient JavaScript Socket.IO
- Backend (serwer przekaźnikowy): socketioxide — serwer Socket.IO napisany w Rust, oparty na Axum
- Protokół: Socket.IO przez WebSocket (z automatycznym przejściem na HTTP long-polling w razie potrzeby)
Socket.IO został wybrany zamiast surowego WebSocket, ponieważ zapewnia automatyczne ponowne łączenie, zarządzanie pokojami/przestrzeniami nazw oraz ustrukturyzowaną obsługę zdarzeń od razu po wdrożeniu.
Przebieg gry
Dział zatytułowany „Przebieg gry”1. Tworzenie gry
Dział zatytułowany „1. Tworzenie gry”Gospodarz klika Multiplayer i wpisuje swoją nazwę wyświetlaną. Klient emituje zdarzenie create_game z nazwą. Serwer:
- Generuje unikalny 6-znakowy kod pokoju
- Tworzy pokój i dodaje gospodarza jako pierwszego gracza
- Odpowiada zdarzeniem
game_created(code), aby gospodarz mógł udostępnić kod
2. Dołączanie do gry
Dział zatytułowany „2. Dołączanie do gry”Dołączający gracz wpisuje kod pokoju i swoją nazwę wyświetlaną. Klient emituje zdarzenie join_game(code, name). Serwer:
- Wyszukuje pokój po kodzie
- Dodaje dołączającego jako drugiego gracza
- Wysyła
game_joined(name)do obu graczy — każdy otrzymuje nazwę wyświetlaną przeciwnika
3. Rozgrywka
Dział zatytułowany „3. Rozgrywka”Gdy obaj gracze są w pokoju:
- Gospodarz zawsze gra białymi, dołączający zawsze gra czarnymi. Decyduje o tym kolejność dołączenia, a nie negocjacja.
- Ruchy są wysyłane w formacie UCI (np.
e2e4) wraz z czasami zegarów obu graczy. - Serwer przekaźnikowy przekazuje każde zdarzenie
game_movedo przeciwnika. Obaj gracze również otrzymują z powrotem swoje własne ruchy jako potwierdzenie.
4. Zdarzenia końca gry
Dział zatytułowany „4. Zdarzenia końca gry”Kilka zdarzeń obsługuje scenariusze zakończenia gry:
- Poddanie — przekazywane do przeciwnika w celu wyświetlenia okna dialogowego poddania
- Propozycja remisu — przekazywana do przeciwnika, który może ją zaakceptować lub zignorować
- Akceptacja remisu — przekazywana do obu graczy w celu zakończenia gry remisem
5. Rewanż
Dział zatytułowany „5. Rewanż”Po zakończeniu gry każdy z graczy może wysłać zdarzenie ready, sygnalizując chęć rewanżu. Gdy obaj gracze wyślą ready, klienci resetują szachownicę i zamieniają kolory (lub je zachowują — o tym decyduje strona klienta).
Kody pokojów
Dział zatytułowany „Kody pokojów”Kody pokojów składają się z 6 znaków, sformatowanych jako XX-XX-XX dla czytelności przy ustnym przekazywaniu. Zestaw znaków wyklucza wizualnie niejednoznaczne znaki:
- Brak
0aniO(zero a litera O) - Brak
1,IaniL(jedynka a wielkie I a wielkie L)
Pozwala to uniknąć problemu „czy to zero, czy O?” gdy ktoś odczytuje kod przez czat głosowy lub przepisuje go z wiadomości od znajomego.
System heartbeat
Dział zatytułowany „System heartbeat”Połączenia wieloosobowe muszą wykrywać, kiedy gracz się rozłączy — czy to z powodu utraty połączenia sieciowego, zamknięcia aplikacji, czy uśpienia laptopa. En Parlant~ wykorzystuje do tego system heartbeat:
- Każdy klient wysyła zdarzenie
heartbeatdo serwera co 5 sekund - Serwer potwierdza heartbeat i przekazuje go do przeciwnika jako
peer_heartbeat - Klient śledzi, kiedy ostatnio otrzymał
peer_heartbeatod przeciwnika - Funkcja
isPeerAlive(timeoutMs)sprawdza, czy ostatni heartbeat przeciwnika mieści się w dopuszczalnym progu czasowym
To napędza wskaźnik stanu połączenia w interfejsie użytkownika. Jeśli heartbeaty przestaną przychodzić, gracz widzi, że jego przeciwnik mógł się rozłączyć, i może zdecydować, czy czekać, czy opuścić grę.
Czyszczenie pokojów
Dział zatytułowany „Czyszczenie pokojów”Serwer przekaźnikowy automatycznie usuwa nieaktywne pokoje, aby zapobiec wyciekom pamięci:
- Pokój jest uznawany za nieaktywny po 30 minutach bez aktywności
- Zadanie czyszczenia uruchamia się co 60 sekund, usuwając wszelkie pokoje, które przekroczyły próg nieaktywności
- Gdy pokój jest usuwany, wszystkie pozostałe połączenia są zamykane
Nie ma trwałego przechowywania danych. Jeśli serwer zostanie zrestartowany, wszystkie pokoje znikają. Jest to celowe — serwer przekaźnikowy jest bezstanowy i efemeryczny. Gry w toku musiałyby zostać rozpoczęte od nowa, ale w praktyce zdarza się to rzadko.
Wdrożenie
Dział zatytułowany „Wdrożenie”Domyślny serwer przekaźnikowy działa na Fly.io, zapewniając połączenia WebSocket o niskim opóźnieniu z automatycznym TLS. Zapoznaj się z przewodnikiem konfiguracji serwera wieloosobowego, aby dowiedzieć się, jak uruchomić własny serwer przekaźnikowy.
Testowanie lokalne
Dział zatytułowany „Testowanie lokalne”Aby przetestować tryb wieloosobowy podczas pracy deweloperskiej:
-
Sklonuj i uruchom serwer przekaźnikowy:
Okno terminala git clone https://github.com/DarrellThomas/en-parlant-relay.gitcd en-parlant-relaycargo runSerwer uruchamia się na porcie 3210.
-
W En Parlant~ zmień adres URL serwera przekaźnikowego na
ws://localhost:3210. -
Otwórz dwie instancje aplikacji (lub jedną aplikację i jedną w trybie deweloperskim), aby zasymulować obu graczy.
Ruchy, heartbeaty i wszystkie zdarzenia działają identycznie jak na produkcyjnym serwerze przekaźnikowym — jedyną różnicą jest adres URL połączenia.