Semestrální práce z UPS
autor: jan bařtipán / a03043 / bartipan_na_students_puntik_zcu_puntik_cz
Zadání | uživatelská dokumentace | programátorská dokumentace | download | verze pro tisk
Zadání
Základní zadání č. 17
Počítačové hry
- Vytvořte program realizující hru lodě, piškvorky, odebírání zápalek apod.
- Pro realizace využijte vhodného grafického rozhraní.
- Využijte služeb UNIXu a BSD socketů.
Hadi - síťová multiplayerová hra na způsob tronu
Uživatelská dokumentace
Server
Ke spuštění serveru slouží binarka s prozaickým názvem server. Má jeden nepovinný vstupní parametr - název souboru, do kterého se případně bude ukládát záznam komunikace serveru s klienty (bližší popis viz programátorská dokumentace).
Po spuštění server ještě potřebuje znát počet hráčů. Na tuto informaci se vyptá v konzoli. Akceptuje celé číslo v rozmezí 2 až 16. Poté čeká na připojení všech klientů. V průběhu hry je otevřeno okno pozorovatele, ve kterém je vidět průběh hry. Server nelze ukončit, dokud porbíhá hra. Po ukončení hry vyčkává na stisk klávesy.
Klient
Ke spuštení klienta slouží binárka hadice. Opět má jeden nepovinný vstupní parametr - název souboru, do kterého se bude případne bude ukládat záznam komunikace se serverem. Po spuštění se klient nejprve zeptá na jméno serveru (akceptuje jak doménové jméno tak i IP adresu). a na nick, pod kterým bude hráč ve hře vystupovat. Hada hráč ovládá kurzorvými klávesami vlevo a vpravo. Pokud zvítězí hráč, má právo "spanilé jizdy", kterou ukončí vlastní sebevraždou, či stiskem klavesy escape. V průběhu hry se může hráč odpojit. Učiní tak stiskem klávesy escape.
Programátorská dokumentace
Koncept komunikace
Hra je závislá na předchozím stavu. To znamená, že je potřeba koordinace všech hráčů. Proto server i klient běží v jediném procesu. Synchronizace je pak docíleno blokováním chodu programu nad čtením dat ze sítě.
Protokol
Komunikaci začíná klient, zasláním zprvá hello, která má následující tvar:
int nick_len // délka nicku
char nick[nick_len] // pole znaků o délce nick_len, které nese informaci o nicku
Server vyčká, dokud se takto nepředstaví všichni klienti a pak každémů z nich odpoví následující zprávou:
int no_players // počet hráčů
int x,y,fi // inicializační stav klientova hada
Dále následuje seznam nicků ostatních hráčů (je zřejmé, že tato zprává bude zopakována no_players-krát:
int nick_len // opět délka nicku
char nick[nick_len] // nick
Touto zprávou říká server, že hra právě začala.
V každém kroku (tahu) hry posílá klient následující zprávu:
int x,y,fi // aktuální stav hráče
bool end_request // je-li nastaveno na true, žádá tím klient o ukončení hry, na kterou server již neodpovídá, pouze ukončí spojení
Pokud položka end_request byla nastavena na false, server pokračuje v komunikaci až v okamžiku, kdy předchozí zprávu zašlou všichni klienti. Formát zprávy serveru je následující:
int game_state // stav hry: 0 - hra probíhá; 1 - hra je u konce
int your_score // aktuální skóre hráče
bool r_u_alive // true - pokud hráč je stále naživu (nekolidoval s jiným hráčem nebo se nepokusil opustit hrací plochu)
Bez ohledu na hodnotu položky game_state server pokračuje v komunikaci ješte posloupností zpráv, kterými informuje klienta o stavu ostatních hráčů. Pro každého hráče (vyjma právě obsluhovaného klienta) posílá server zprávu v tomto formátu:
int x,y // pozice soupeře (kam popolezl)
int score // skóre soupeře
Teď již má klient veškeré informace, které v tomto tahu potřebuje (čímž je tento tah ukončen) a opět posílá zprávu o svém stavu...
Struktura systému
Pro síťovou komunikaci jsem vytvořil knihovnu netstream, která má následující objetky:
- třída Socket - implementuje operace pro čtení, zápis a uzavření spojení. Sama o sobě se nepoužívá, je rodičem pro třídy ClientSocket, ServerSocket a AcceptedSocket. Má následující metody:
void get(char* buffer, int size) // čte ze soketu data (buffer) o délce size
void set(char* buffer, int size) // zapisuje data (buffer) o délce size do soketu
void destroy() // uzavírá socket
- třída ClientSocket - je potomkem třídy Socket. Slouží pro navázení spojení klienta se serverem. K tomu jí slouží tento konstruktor:
ClientSocket( Host host) // kde host je objekt reprezentující informaci o adrese serveru
- třída ServerSocket - opět je potkem třídy Socket. Slouží pro naslouchání serveru na portu. Má následující metody
ServerSocket(unsigned int port) // sváže soket s portem
AcceptedSocket wait() // čeká na připojení klienta. Ke komunikaci s ním slouží vrácená instance třídy AcceptedSocket
- třída AcceptedSocket - potomek třídy Socket, slouží ke komunikaci serveru s klientem. K tomuto stačí metody pro čtení a zápis z předka Socket
- třída Host - reprezentuje informaci o adrese serveru:
Host(char* host, unsigned int port) // host - doménové jméno či IP adresa stroje
// port - port pro připojení
- třída Exception - pokud nastane při síťové komunikaci chyba, je vyvolána výjimka Exception
Exception(char *fmt, ...) // konstruktor, fmt - formátovací řetězec (stejný jako u printf)
Exception(char *msg) // konstruktor, msg - text chybové zprávy
void print() // vypíše příčinu výjimky
Knihovna netstream má ještě dvě funkce
- void ns_init() // inicializuje knihovnu
- void ns_done() // ukončuje knihovnu (pozavírá všechny otevřené sokety) a vypíše statistické údaje
Ke svému chodu jak server tak i systém využívá další dva moduly:
- modul misc - obsahuje rutiny pro zadávání řetězce a čísla z konzole a rutiny pro logování do souboru:
int prompt_nubmer(const char *msg, int low, int high) // vstup čísla z konzole, msg - prompt,
// low - nejnižší přípustná hodnota,
// high nejvyšší přípustná hodnota, vrací načtené číslo
void prompt_string(const char *msg, char *ret, int ret_len) // vstup řetězce z konzole, msg - prompt,
// ret - místo kam se uloží načtený řetězec,
// ret_len - maximální přípustná délka řetězce
// (při překročení dojde k oříznutí)
void log_file_open(char* filename) // otevře logovací soubor jména filename
void log_file_print(char *fmt, ...) // obdobně jako printf, zapisuje do logovacího souboru
void log_file_close() // uzavře logovací soubor
- modul net - slouží jako mezivrstva mezi knihovnou netstream a systémem pro komunikaci klienta se serverem a serveru s klientem
obsahuje následující strutktury:
typedef struct client_hello{
char nick_len; // length of nick
char* nick; // nick itself
} client_hello;
typedef struct server_hello_head{
int no_players; // count of players
int x,y,fi; // initial state of player
} server_hello;
typedef struct server_hello_tail{ // per player (Excluding local) hello message
char nick_len; // length of nickname
char* nick; // nickname itself
} server_hello_tail;
typedef struct client_info{
int x,y,fi; // actual state of player
bool end_request; // request to end game
} client_info;
typedef struct server_info_head{
int game_state; // state of game 0 means that game's runnin' 1 - game is endin'
int your_score; // score of player
bool r_u_alive; // are you still alive?
} server_info_head;
typedef struct server_info_tail{ // per player (excluding local) info
int x,y; // state of other players
int score; // score of players
} server_info_tail;
Dále pak obsahuje rutiny pro klienta i servera k přijímání a odesílání zpráv. Jména těchto rutin se skládájí z prefixu, který určuje, zda rutina je pro odesílání (send_) nebo přijímání (recv_) dat, dále s kým komunikujeme (server_) či (client_) a jaký typ zpravy mu chceme poslat (např. info_head). Pro úplnost tedy uvádím seznam těchto rutin:
void send_client_hello(Socket s, client_hello msg);
void recv_client_hello(Socket s, client_hello *msg);
void send_server_hello_head(Socket s, server_hello_head msg);
void recv_server_hello_head(Socket s, server_hello_head *msg);
void send_server_hello_tail(Socket s, server_hello_tail msg);
void recv_server_hello_tail(Socket s, server_hello_tail *msg);
void send_client_info(Socket s, client_info msg);
void recv_client_info(Socket s, client_info *msg);
void send_server_info_head(Socket s, server_info_head msg);
void recv_server_info_head(Socket s, server_info_head *msg);
void send_server_info_tail(Socket s, server_info_tail msg);
void recv_server_info_tail(Socket s, server_info_tail *msg);
zdrojový soubor main.cpp - klient
Hlavní modul pro klienta (soubor main.cpp) má tyto globální proměnné:
int x,y,fi,fiadd; // stav lokálního hráče
bool alive; // je stále naživu?
int score; // jeho skóre
bool end_request; // požádal o ukonční sezení
int game_state // stav hry (0 - běží, 1 - končí)
typedef struct{ // struktura udržující stavy vzdálených hráčů
int x, y, score;
char *nick;
} remote_player;
int remote_players_count; // počet vzdálených hráčů
remote_player remote_players[MAX_PLAYERS]; // jejich stavy
Dále se v tomto modulu nachází funkce, které globální proměnné posílají serveru nebo zprávy ze serveru ukládájí do globálních proměnných:
void send_hello(Socket s, char *nick) // pošle hello serveru
void recv_hello(Socket s) // uloží hello do globálních proměnných
void send_info(Socket s) // pošle stav lokálního klienta serveru
void recv_info(Socket s) // získá stavy vzdálených hráčů ze serveru
A konečně se zde nachází funkce na zobrazování hráčů a obsluhu kláves
void handle_keys() // obsluha kláves
int xa_calc(float x) // na základě úhlu fi (x) vypočte x-ový přírůstek
int ya_calc(float x) // na základě úhlu fi (x) vypočte y-ový přírůstek
void step() // na základě fi nastaví nový stav
void draw() // vykreslý hady
void init() // inicialzace globálních proměnných
A nesmí chbět funkce main:
int main(int argc, char** argv) // <--- to je ona
zdrojový soubor server_main.cpp - server
Takto jsou uloženy stavy všech hráčů:
typedef struct{
int x,y,fi; // state
int score; // score of player
bool alive; // is alive?
bool connected; // is connected?
AcceptedSocket s; // communication socket
char* nick; // nick of player
} remote_player;
int remote_players_count; // count of players
remote_player remote_players[MAX_PLAYERS];
I zde jsou funkce, které komunikují po síti (tentokrát s klienty) a to buď tak, že globální proměnné posílají klientům a nebo stavy klientů zaznamenávají
do globálních proměnných:
void recv_hello(int no) // přijme hello zprávu od klienta číslo no
void send_hello(int no) // pošle hello zprávu klientovy číslo no
void recv_info(int no) // získá stav klienta
void send_info(int no, int game_state)// pošle stav ostatních hráčů a stav hry
Dále tu máme funkce, které obstarávájí různé úkony nutné pro hru
void inc_score_of_alive_players() // zvýší skóre všem živým hráčům (voláno v okmažiku umrtí hráče)
void init_players() // inicializuje stavy všech hráčů
void done_players() // obešle všechny hráče s informací, že hra končí
bool exists_alive_player() // zjistí, zda existuje ještě hráč, který je naživu
bool exists_connected_player() // zjistí, zda existuje ještě hráč, který je připojen
int xa_calc(float x) // vypočte x-ový přírůstek na základě úhlu fi (x)
int ya_calc(float x) // vypočte y-ový přírůstek na základě úhlu fi (x)
bool collide(int no) // funkce, která zjišťuje, zda nějaký z hráčů nehavaroval a podle toho nastaví příznak alive
void step(int no) // nastavý nový stav hráče číslo no
void draw() // vykreslí všechny hráče
int main(int argc, char** argv) // <--- funkce main, co více říci
Download
hadice-0.1.tar.bz2 - zdrojové kódy hry, vyžadují knihovnu allegro