Specyfikacja Po-Implementacyjna
Diagramy klas
Główne klasy silnika
Silnik Slick użyty w grze wymaga utworzenia klasy implementującej intefejs Game i umieszczenia jej w stosownym dla sposobu uruchomienia, kontenerze. Klasa uruchomieniowa Application wykonuje te właśnie zadania i jest zarazem singletonem, przez co pomaga innym klasom w prosty sposób dostawać się do przydatnych zasobów bez konieczności uciążliwego przekazywania uchwytów między obiektami.
Przejścia między ekranami dokonują się dzięki zastosowaniu wzorca projektowego Stan, którego dogodna implementacja jest także w Slick'u. Poszczególne ekrany implementują odpowiedni interfejs, a klasa Game deleguje wywołania odpowiednich metod do aktualnego stanu.
Warstwa komunikacji
Główna część komunikacji odbywa się poprzez RMI (Remote Method Invocation), dodatkowo do wyszukiwania serwera w sieci lokalnej użyliśmy niskopoziomowych gniazd bazujących na protokole UDP (obsługa zawarta w klasie Broadcast).
Założyliśmy, że będzie istniał zaszysty interfejs serwer, który będzie przetwarzał wszelkie żądania i rozsyłał wyniki przetwarzania po klientach (w tym także kliencie lokalnym). Dodatkowo ustaliliśmy, że w ramach zachowania porządku, jedynym miejscem, w którym będzie szło żądanie po sieci, będzie złącze między klientem a serwerem. To oznacza, że interfejs będzie się łączył z serewerem za pośrednictwem klientów, a serwer będzie przekazywał żądania zmian modelu także do klientów. W niektórych przypadkach założenie to było uciążliwe (nadmiarowy kod) i dlatego postanowiliśmy się zrobić pewne odstępstwa poprzez wydzielenie ServerLocal i ServerLocalModel.
Dostęp do intefejsu ServerLocal istnieje poprzez wywołanie Client.getServer(). ServerLocalModel jest interfejsem storzonym z myślą o zdarzeniach modelu (nie użytkownika). Zdarzenia te wędrują bezpośrednio do Serwera, ale tylko w przypadku gdy model jest modelem lokalnym (po stronie serwera). W celu ułatwienia tego procesu zastosowaliśmy wzorzec Null Object i Dynami Proxy
private static ServerLocalModel NULL_SERVER; public static ServerLocalModel getNULL_SERVER() { if (NULL_SERVER == null) { NULL_SERVER = (ServerLocalModel) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class[]{ServerLocalModel.class}, new InvocationHandler() { public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable { return null; } }); } return NULL_SERVER; }
Klasa serwer przekazuje żądania zmian modelu do klasy MessageBroker która rozsyła je po klientach.
Warstwa gry właściwej
Klasą główną dla gry właściwej jest GamePlay. Rysowanie poszczególnych obiektów jest dokonywane poprzez kolejne delegowanie wywołan render() do subkomponentów. Począwszy od Camera, która centralizuje widok na graczu i wywołuje render() na World, a następnie na VisibleRange - przysłania obszar niewidoczny, Hud - kokpit gracza. Natomiast World renderuje obiekty statyczne (podłoże, ściany, postacie) i ruchome (postacie przemieszczające się).
Do tworzenia obiektów modelu używany jest ObjectsFactory
Czynności połączenia i negocjacji
Poniższy diagram w czytelny sposób przedstawia jak dokonuje się połączeni i negocjacja gry (wymiana informacjami o preferencjach, wyrzucenie gracza, rezygnacja, czy też zatwierdzenie)
Sekwencje komunikacji
Połączenie i negocjacja
Ten sam scenariusz, tylko przedstawiony w wariancie pesymistycznym (sewer jest od razu uruchomiony, żaden z graczy nie ma zastrzeżen co do preferencji gracza przeciwnego).
Obsługa zdażenia ruchu
Zapewnienie synchronizacji gry między graczami przez sieć jest niewątpliwie trudnym zadaniem i można jego dokonać na wiele sposobów. My przyjęliśmy założenia:
- Metody serwera są synchroniczne wątkowo w celu zapewnienia spójności modelu po stronie serwera (przy przykładowym wywołaniu move() klasa serwer wykonuje sprawdzenie, czy ruch jest możliwy i dopiero później przekazuje zmiany do klientów. Jeśli serwer nie byłby synchroniczny mogło by dojść do naruszenia spójności (wątek obsługujący odbieranie wywołan RMI nie jest tym samym, co wątek główny gry)
- Serwer zajmuje się całym przetwarzaniem zdażeń (na wejściu dostaje zdarzenie klawiatury, albo zdarzenie modelu lokalnego), a na wyjściu rozsyła informacje o kolizjach, przesunięciach itp. po klientach
- Serwer wysyła informacje synchronizujące, np. przy wysłaniu ruchu, nie wysyła tylko id obiektu i wektor kierunku, ale zamiast tego punkt startowy i docelowy. Jeżeli po stronie klienta obiekt znajduje się w innym położeniu, model zostanie zsynchrozowany.
- Serwer dokonuje zmian najpierw na kliencie lokalnym (synchronicznie bez wprowadzania nowych wątków) a do pieropotem na kliencie zdalnym (asynchronicznie - wątki w puli). Znowu daje to przejrzystość architetktury i nie dopuszcza do utraty spójności modelu lokalnego, a zarazem nie blokuje klasy serwer (rozwiązanie asynchroniczne).
- Zdarzenia klienta mogą nie zostać dostarczone (np. chwilowa niedostępność serwera), ale informacje o zmianach w modelu muszą zostać dostarczone (kolejki i ponawianie)






