Simple Online GAME

Transkrypt

Simple Online GAME
Politechnika Warszawska Wydział Elektryczny
Kamil Deryński
Nr albumu: 197570
Simple Online GAME
Projekt koncepcyjny gry sieciowej.
Praca na projekt indywidualny w roku 2007r.
Kierunek Informatyka (semestr 4).
Praca wykonana pod kierunkiem
mgr inż. Konrada Andrzeja Markowskiego,
Instytut Sterowania i Elektroniki Przemysłowej.
Maj 2007
Streszczenie:
W pracy tej przedstawiam koncepcję i konkretny zarys gry
sieciowej, która to była tematem projektu indywidualnego. W
poszczególnych działach opiszę wybrane technologie użyte
podczas realizacji projektu, a także przedstawię powszechnie
stosowane schematy konstrukcji takich aplikacji. Następnie
opisuję szczegóły programu oraz problemu występujące podczas
prac.
Wprowadzenie:
Gry sieciowe są naturalnym rozszerzeniem gier trybu
jednoosobowego. Umożliwiają wspólną rozgrywkę dla więcej niż
jednego gracza w tym samym czasie. Wraz z ewolucją i ekspansją
Internetu tzw. tryb Multiplayer jest nieodłącznym elementem
każdej gry. Popularyzację na szeroką skale gry wieloosobowej
przez sieć zawdzięczamy głównie grą typu FPS (First Person
Shooter),
przedstawicielem
takiego
gatunku
jest
Quake,
pierwsza
część
tej
gry
oferowała
już
bardzo
dużą
funkcjonalność dotyczącą rozgrywki wieloosobowej, wydajne
serwery umożliwiające grę wielu uczestników jednocześnie
dołączanych do gry podczas toczącej się już rozgrywki, a także
optymalizowany przepływ danych, co pozwoliło na wymianę wielu
danych dla obsługi skomplikowanego środowiska. (QuakeWorld)
Weryfikacja wstępnych założeń projektu1:
Projekt
nie
został
zakończony
wykonaniem
w
pełni
działającej gry sieciowej, co jest naturalną implikacją nie
spełnienia wszystkich założeń projektu, jednak prace trzymały
się
głównych
przedstawionych
wcześniej
założeń,
które
wypunktuję poniżej:
Założenia funkcjonalne:
1. Program umożliwia grę 2 osobową
2. Warstwa sieci aplikacji opiera się na modelu klientserwer.
3. Rozgrywka toczy się w czasie rzeczywistym.
4. Zarówno serwer jak i klient wbudowany jest w program,
dokonujemy wyboru, jaką rolę będzie pełnić jedna z
instancji programu.
5. Budowa
gry
umożliwia
implementowanie
typu
zręcznościowego.
1
Wstępne założenia projektu dostępne w pliku WstępneZalozenia.pdf
2
Założenia implementacyjne:
1. Język programowania – cała aplikacja napisana została z
wykorzystaniem języka C++ z użyciem STL (Standard
Template Library).
2. Wykorzystane biblioteki – w początkowych założeniach
miała być to biblioteka Allegro jednak zdecydowałem się
ostatecznie na dobrze mi znaną przenośną bibliotekę SDL z
pomocą
biblioteki,
SDL_gfx
do
rysowania
prymitywów
graficznych.
3. Warstwa sieciowa - SDL_net jako „wraper” programowania
gniazd sieciowych dający pełną przenośność programu.
4. IDE – Microsoft Visual Studio C++ Express wraz z
kompilatorem MS dostarczanym w pakiecie.
Wprowadzenie
narzędzi:
do
użytych
bibliotek
oraz
Biblioteka SDL:
SDL(Simple DirectMedia Layer) – to biblioteka ułatwiająca
tworzenie
gier
oraz
programów
multimedialnych.
Posiada
oficjalne wsparcie dla ponad 20stu systemów operacyjnych w tym
tych najpopularniejszych, czyli MS Windows, GNU/Linux, BSD, a
także tych niszowych jak choćby AmigaOS czy też QNX. SDL nie
oferuje
wysokopoziomowych
funkcji,
jest
raczej
warstwą
gwarantującą przenośność. Do tego celu właśnie została
stworzona (LGPL) w 1998r przez Sama Lantiga z firmy Loki.
Przykładowe gry korzystające z SDL:
-
Battle for Wesnoth
Frozen Bubble
Neverwinter Nights
Quake4 (tylko w wersji pod Linuksa)
Ogólny schemat działania, SDL:
3
SDL_gfx
–
biblioteka,
która
wyewoluowała
z
kodu
SDL_gfxPrimitives obecnie oferuje znacznie więcej niż tylko
udostępnianie funkcji do rysowania prymitywów. Biblioteka
została napisana przez Andreas Schifflera, przez, którego
udostępniana jest na licencji LGPL.
Komponenty biblioteki SDL_gfx:
•
•
•
•
Graphic Primitives (SDL_gfxPrimitves.h)
Rotozoomer (SDL_rotozoom.h)
Framerate control (SDL_framerate.h)
MMX image filters (SDL_imageFilter.h)
SDL_net – biblioteka, dzięki, której można uzyskać przenośność
programów sieciowych pisanych z wykorzystaniem gniazd. Uwalnia
programistę z konieczności pisania tej samej funkcjonalności
sieciowej
dwa
razy
(implementacja
socketów
POSIX
oraz
implementacja winsock w MS windows).
Microsoft Visual Studio C++ Express – w pełni darmowa wersja
do użytku komercyjnego środowiska programistycznego firmy
Microsoft. Wersja Express jest ograniczona brakiem kilku
funkcji oraz innym kompilatorem, który produkuje pliki
wykonywalne wymagające pakietu Redistribution na komputerze,
na którym chcemy uruchomić skompilowany program. Jednak
niedogodność kompilatora można ominąć instalując darmowy
Toolkit 2003. Niezaprzeczalną zaletą MS VS jest obecność
zintegrowanego bardzo dobrego debugera, który znacznie ułatwia
wykrywanie błędów.
TortoiseSVN – do zarządzania wersjami programu oraz do
sprawniejszego
panowania
nad
kodem
używałem
SubVersion,
systemu kontroli wersji. Moje repozytorium jest dostępne w
Internecie pod adresem svn://dragon.dust.pl/osg (we wczesnej
fazie rozwoju do repozytorium nie ma publicznego dostępu). Do
korzystania z SubVersion używam programu TortoiseSVN, który
integruje się z interfejsem systemu Windows i umożliwia pełną
obsługę możliwości udostępnianych przez serwer SVN.
STL(Standard Template Library) – standardowa biblioteka zgodna
ze standardem ANSI języka C++. Umożliwia programowanie
generyczne oraz m.in. dostęp do złożonych typów danych, a
także udostępnia cały wachlarz algorytmów oraz dynamicznych
struktur danych.
4
Opis enginu gry:
Główny engine, na którym opiera się gra zawiera się w
pliku engine.h, engine.cpp. Pliki te zawierają opis oraz
implementacje klasy silnika gry, który dzieli się na kilka
głównych części:
1. Główne zmienne i dane (powierzchnia ekranu, dane okna,
zmienna wyjściowa, manager „Frame Limitera” itd.)
2. Metody głównego enginu: Init, Start, DoRender, DoThink
(odpowiadające
inicjalizacji
bibliotek
obsługi,
inicjalizacja enginu, podstawowe funkcje renderujące,
podstawy obliczeń oraz obsługa defaultowych akcji).
3. Cała gama metod wirtualnych: dzięki nim w klasie
dziedziczącej
po
klasie
engine,
można
definiować
specyficzne dla implementacji konkretnej gry cechy).
Listing definicji klasy engine (engine.h):
#ifndef ENGINE_H
#define ENGINE_H
#include "SDL.h"
#include "SDL_NET.h"
#include "SDL_framerate.h"
class CEngine {
private:
int m_iWidth;
int m_iHeight;
bool m_bQuit;
const char* m_czTitle;
SDL_Surface* m_pScreen;
FPSmanager* fps_manager;
protected:
void DoThink();
void DoRender();
void SetSize(const int& iWidth, const int& iHeight);
void HandleInput();
public:
CEngine();
virtual ~CEngine();
void Init();
void Start();
virtual void AdditionalInit
virtual void Think ( ) {}
() {}
5
virtual
virtual
virtual
virtual
void
void
void
void
Render( SDL_Surface* pDestSurface ) {}
End() {}
KeyUp (const int& iKeyEnum) {}
KeyDown (const int& iKeyEnum) {}
void SetTitle (const char* czTitle);
const char* GetTitle();
SDL_Surface* GetSurface();
};
#endif // ENGINE_H
Klasa myEngine, która dziedziczy po klasie engine wzbogaca
go o m.in. obsługę sieci. W obiekcie klasy myEngine powoływane
są między innymi obiekty klas CListener oraz CSender, które są
interfejsami sieciowymi dla instancji serwera oraz klienta
aplikacji. Obiekty te udostępniają metody służące do wysyłania
danych oraz odbierania (void mySend(int,int,int) oraz void
listen(void)).
Realizacja warstwy sieciowej:
Realizacja przesyłu danych wykonywana jest przy pomocy
gniazd datagramowych, a więc wykorzystywany jest protokół UDP,
który jest bezpołączeniową metodą komunikacji w sieci typu IP.
Model wykorzystany do „rozmowy” aplikacji między sobą to
tradycyjny klient-serwer.
Schemat modelu klient-serwer:
W aplikacji został zaimplementowany typowy model aplikacji
sieciowej typu klient-serwer.
Serwer tworzy gniazdo sieciowe, a następnie przypisuje go
do adresu sieciowego komputera oraz do podanego numeru portu,
na którym chcemy, aby nasz serwer nasłuchiwał. Po otrzymaniu
informacji serwer przetwarza otrzymane dane, a następnie
odsyła te dane nadawcy.
Klient właściwie nie różni się bardzo od serwera. Jednak
elementy odróżniające implementacje klienta od implementacji
serwera to: otwieranie losowego portu z zakresu wysokich
portów (SDLNet_UDP_Open(0) – argument 0 oznacza port losowy),
Bindowanie socketu na podany adres serwera oraz jego port
ostatnia różnica to zamieniona kolejność wywoływania funkcji
wysyłania i odbioru – klient najpierw wysyła dane, a następnie
oczekuje przetworzonych danych przez serwer, do którego
kierował żądanie.
6
socket
socket
bind
bind
recv
Dane
(żądanie)
send
Dane
(odpowiedź)
recv
Przetwarza
nie
żądania
send
Rys. Uproszczony schemat modelu klient-serwer.
Definicja klasy właściwego „serwera” UDP:
#ifndef UDP_SERVER_H
#define UDP_SERVER_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "SDL_net.h"
class CUDP_server {
private:
UDPsocket sd;
IPaddress srvadd;
UDPpacket *p;
public:
7
CUDP_server(char* host, int port);
char* getData();
void setData(char* inputData);
void restart();
};
#endif
Opis pakietu UDP typu UDPpacket:
p->channel
p->data
p->len
p->maxlen
p->address.host
p->address.port
–
-
kanał pakietu (priorytety).
dane, które niesie pakiet.
długość danych
maksymalna długość danych
adres hosta
numer portu
-
Wydruk z logu serwera gry:
UDP Packet incoming
Chan:
-1
Data:
400;300;0;
Data sizeof:
4
Len:
10
Maxlen: 32
Status: 10
Address: a00a8c0 3704
Wydruk pakietu w postaci ASCII wczesna wersja programu.
ASCII - @|[CnE$T[l400;300;
Po nagłówku pakietu UDP następują dane przesyłane z klienta do
serwera (zaznaczone na czerwono).
Zrzut ekranu na podgląd pakietu ze snifera (Wireshark)
Dlaczego UDP ?
Protokół UDP jest bezpołączeniowy, a więc nie wykorzystuje
się tu mechanizmu połączeń i mechanizmu „3-krotnego uścisku
8
dłoni”. Dzięki temu UDP jest znacznie szybsze niż połączenia
TCP, szacuje się, że dla rozwiązań wymagających powyżej
4 „połączeń” na sekundę użycie protokołu bezpołączeniowego
jest znacznie korzystniejsze. Jednak najważniejsza wadą UDP
jest brak potwierdzenia odbiorcy o właściwym otrzymaniu
pakietu. Możliwa jest, więc utrata pakietu bez wiedzy nadawcy.
Nie jest to problemem przy „nadawaniu” kilku pakietów na
sekundę,
ponieważ
informacje
nadmiarowe
uzupełniające
ewentualny
brak.
Problem
pojawia
się
w
przypadku
synchronizacji gry dzięki numerowi klatki, w którym wykonywana
jest
dana
akcja.
Trzeba,
więc
używać
nie
poklatkowej
synchronizacji oraz zapewniać podstawową kontrole przepływu
pakietów UDP między klientem a serwerem. Oba te warunki
gwarantują niezawodne działanie całego mechanizmu.
Główne problemy i metody ich rozwiązania:
Frame Rate Limiter, problem szybkości?
Silnik gry opiera się na głównej „ciasnej” pętli, której
zadaniem
jest
uaktualnianie
danych
środowiska
gry,
wykonywaniem cyklicznych obliczeń, wywoływaniem interfejsów
komunikacyjnych oraz renderingu „świata” gry. Komputer stara
się wykonywać program jak najszybciej może, a więc szybkość
programu bezpośrednio zależy od częstotliwości taktowania
zegara procesora. Wniosek jest prosty i naturalny im szybszy
procesor, tym szybciej program będzie działać, a co za tym
idzie tym częściej będzie wykonywana główna pętla enginu gry.
Gdyby program uruchamiany był zawsze na identycznych
maszynach nie było by tego rozdziału, problem by po prostu nie
istniał. Twórcy gier z przełomu lat 80 i 90tych często nie
zadawali sobie trudu, aby w ogóle ten problem rozwiązywać.
Stąd też zjawisko super szybkiego działania niektórych starych
gier na współczesnych komputerach, w wyniku powoduje to
kompletną nie możliwość gry (trudność opanowania tak szybkiego
rozwoju sytuacji przez człowieka).
We współczesności problem regulacji szybkości gry jest
szczególnie ważny w obliczu ogromnych różnic w mocach
obliczeniowych obecnie dostępnych maszyn. Nikogo dziś nie
dziwią różnice mierzone w gigahercach, a o procesorach
wielordzeniowych nie wspominając.
Nie mniej ważnym powodem dla celowości normalizacji
szybkości
działania
gry
jest
fakt
komunikacji
różnych
instancji programu na różnych maszynach przez sieć. Gdy jeden
gracz będzie dysponował mocnym sprzętem, a jego partner
słabszym nie może przecież zaistnieć sytuacja, gdy program
uruchomiony na szybszym komputerze będzie działać znacznie
9
szybciej, co wtedy z danymi przekazywanymi od gracza ze słabym
komputerem? Przesyłanie danych będzie znacznie opóźnione nie z
winy gracza. W efekcie jeden z graczy będzie miał nierówne
szanse, komunikacja obu programów będzie nie równomierna
(szybszy
program
będzie
wysyłał
dane
z
większą
„rozdzielczością”). Generalizując, już na etapie tego problemu
rozgrywka będzie całkowicie zdesynchronizowana, gracze będą
grać w dwie różne gry. Oczywiście problem synchronizacji nie
opiera się tylko na znormalizowaniu czasu wykonania programu
jednak jest ważnym czynnikiem tego zagadnienia.
Rozwiązanie problemu normalizacji prędkości wykonywania.
Problem ten można rozwiązać wykorzystując niezawodny
licznik dostępny w komputerze, a więc licznik taktów procesora
do pomiaru czasu i wprowadzeniu na tej podstawie potrzebnych
opóźnień do uzyskania pożądanej prędkości.
Początkowo pomiar czasu i związane z tym kalkulacje
wykonałem na podstawie timerów udostępnianych w podstawowej
bibliotece SDL. Obsługa tej funkcji została zaimplementowana w
główny
engin
gry.
Możliwe
było
wtedy
mierzenie czasu, który upłynął od ostatniego
Timer
wywołania
metody
Think()
,
a
więc
od
-startTicks: int
ostatnich
zmian
w
parametrach
całego
-pausedTicks: int
środowiska.
Jednak
wprowadzanie
na
tej
-paused: bool
podstawie opóźnień wykorzystując pustą pętle
-started: bool
while
lub
korzystniejszą
funkcję
SDL_Delay(int)
nie
dawało
stabilnych
i
<<create>>-Timer()
+start(): void
niezawodnych rezultatów.
Przy rozważaniach i próbach z timerami
implementowałem
nawet
osobną
klasę
obsługującą
timery
w
SDLu
(timer.h
–
reprezentacja UML tej klasy na rysunku obok > ).
+stop(): void
+pause(): void
+unpause(): void
+get_ticks(): int
+is_started(): bool
+is_paused(): bool
Ostatecznie jednak ze wszystkich powyższych rozwiązań
zrezygnowałem na rzecz biblioteki SDL_framerate.h , która
wykorzystuje swoistą interpolację opóźnień, daje bardzo dobre
i niezawodne wyniki, a jej bardzo proste użycie jest jej
niezaprzeczalną zaletą.
Bibliotek SDL_framerate.h udostępnia następujące funkcję:
•
•
•
•
void SDL_initFramerate(FPSmanager * manager);
int SDL_setFramerate(FPSmanager * manager, int rate);
int SDL_getFramerate(FPSmanager * manager);
void SDL_framerateDelay(FPSmanager * manager);
10
Jak widać funkcje te operują na wskaźniku na typ FPSmanager,
który jest powoływany w części głównej inicjalizacji zmiennych
enginu.
Zasada działania SDL_framerate:
Opóźnienie
potrzebne
to
utrzymania
prawidłowego
„tempa”
działania
programu
generuje
funkcja
SDL_framerateDelay,
umieszczamy ją np. w części odpowiedzialnej za renderingu
w głównej pętli programu. Jeśli komputer nie może utrzymać
narzuconego „tempa” ustawia opóźnienie na zero i restartuje
interpolację opóźnienia.
Schemat obliczenia opóźnień w SDL_framerate:
Rys. Schemat zaczerpnięty z dokumentacji SDL_gfx.
11
Synchronizacja.
Synchronizacja gry sieciowej jest podstawowym zagadnieniem
w pisaniu tego typu aplikacji. Przy rozwiązywaniu tego typu
problemów trzeba wziąć pod uwagę wiele czynników mogących
zdesynchronizować grę np. opóźnienia w przesyłaniu danych
przez sieć, różne stany instancji programów działających na
różnych komputerach, utrata danych podczas przesyłania itd.
Synchronizacja zaimplementowana obecnie w programie opiera
się na prostym mechanizmie oczekiwania na transmisję. Jest to
zawodny sposób, który gwarantuje jedynie eliminację opóźnień
na reakcję drugiego gracza w grze. Jest to, więc nie zupełnie
metoda
synchronizacji.
Jednak
zakładając
obecnie
wysoką
„częstotliwość”
informowania
drugą
instancję
programu
o
zmianach można założyć, że rozgrywka jest synchroniczna.
Dalsza
rozbudowa
synchronizacji
będzie
polegała
na
implementacji
komunikatów
z
obsługą
ich
typów
oraz
priorytetów. Wtedy synchronizacja będzie opierała się na
numerze klatki animacji, w której nastąpiła dana akcja. Jest
to powszechnie stosowana praktyka przez programistów gier
sieciowych.
Konkretnym
przykładem
zastosowania
tego
rozwiązania jest popularna gra Starcraft.
Rendering:
Obecnie rendering całego „świata” gry realizowany jest
prymitywną metodą rysowania prostych elementów w buforze
ekranu przy każdym kroku pętli oraz czyszczenia tego bufora
przy odświeżeniu. Jest to tymczasowy sposób mający na celu
wizualizację dotychczasowych dokonań.
Kompilacja projektu:
Visual Studio C++ 2005 Express :
Kompilacja odbywa się w tradycyjny sposób należy otworzyć
plik projektu w VS, a następnie dodać ścieżki do bibliotek
linkowanych dynamicznie SDL, SDL_net, SDL_gfx. Pamiętając, że
należy ustawić ścieżki dla plików h czyli „Include files” oraz
„Library files” dla plików z katalogów lib w poszczególnych
bibliotekach.
12
Rys. Tool>Options>Project and Solutions> VC++ Directories
Po dokonaniu tych ustawień należy zmienić profil, na release,
a następnie wcisnąć klawisz, F7, aby skompilować program. Plik
binarny znajdzie się w katalogu release.
UWAGA: Visual Studio 2005 Express generuje pliki binarne,
które uruchamiają się na systemach Windows posiadających
odpowiednie pakiety Redistribution Packages oraz odpowiednie
wersje C Runtime Library. Na systemach systemach systemach
najnowszymi
aktualizacjami
nie
będzie
potrzebna
żadna
ingerencja jednak, na niektórych potrzebne będzie instalacja
tych pakietów lub/oraz skopiowanie odpowiednich plików dll.
(katalog dodatkowe_dll).
Uniwersalne binarki można otrzymać kompilując projekt
pełną wersją Visual Studio (oczywiście chodzi o kompilator cl)
lub skorzystanie z darmowego Toolkit 2003 obsługiwanego z lini
komend (cmd).
Parametry z jakimi kompilowany był projekt:
/O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D
"UNICODE" /FD /EHsc /MD /Fo"Release\\" /Fd"Release\vc80.pdb"
/W2 /nologo /c /Zi /TP /errorReport:promet
Kompilacja na innych systemach :
Kompilacja na inne systemy niż windows jest możliwa dzięki
zastosowaniu przenośnych bibliotek. Jednak nie kompilowałem
projektu w innym środowisku niż windows i VS Express, więc nie
będę opisywał tu procesu kompilacji na innych systemach.
13
Uruchomienie:
W katalogu z plikiem uruchomieniowym osg.exe muszą znajdować
się również pliki z dynamicznie linkowanymi bibliotekami:
•
•
•
SDL.dll
SDL_gfx.dll
SDL_net.dll
a, także plik konfiguracyjny o nazwie osg.conf , który musi
zawierać:
• w pierwszej linii adres ip, na którym będzie działał
serwer gry, a więc np. 192.168.1.1
• w drugiej linii numer portu serwera np. 2000
• w trzeciej linii natomiast znajduje się wartość 0
jeśli chcemy, aby aplikacja działała jako klient lub
1 jeśli chcemy uruchomić aplikacje w roli serwera.
Przykładowy plik konfiguracyjny:
Dla serwera:
192.168.0.1
2000
1
Dla klienta:
192.168.0.1
2000
0
UWAGA: Zalecane jest uruchamiać jako pierwszy
aplikacje serwera, a później aplikacje klienta.
kolejności
Sterowanie:
Sterowanie graczem odbywa się przy pomocy kursorów. Gracz
reprezentowany jest jako biały kwadrat, a partner jako
czerwony kwadrat.
Wyjście
okna.
z
programu
przy
pomocy
ESC
lub
poprzez
zamknięcie
Dodatkowe uwagi:
Po zamknięciu aplikacji w katalogu z plikiem exe zostaną
wygenerowane dwa plik stdout.txt i stderr.txt. Zawierają one
diagnostyczne listingi konfiguracji programu oraz listing
pakietów odebranych przez serwer.
14
Rys. Ekran „gry”.
Uwagi końcowe:
Aplikacja nie jest w pełni funkcjonalną grą, a jedynie
demonstruje wstępne możliwości uniwersalnego, enginu gry
sieciowej, jaki staram się stworzyć.
Łatwo można sobie wyobrazić jak proste jest dodanie teraz
logiki dla najróżniejszych gier. Sprowadza się to jedynie do
wypełniania i rozwijania implementacji w zasadzie 3 metod z
klasy my_engin, które odpowiadają za inicjalizacje zmiennych
gry, obliczenia i aktualizację parametrów oraz rendering.
Oczywiście jak wspomniałem rozwój logiki gry nie jest
jedynym zadaniem na „przyszłość”, trzeba zaimplementować
mechanizmy synchronizacyjne oraz zoptymalizować interfejsy
sieciowe aplikacji.
15

Podobne dokumenty