Tematem naszego projektu było stworzenie gry przygodowej czasu rzeczywistego dostępna w trybie multiplayer.
Serwer został zaimplementowany w technologii Delphi.

W zwiazku z tym że nasza gra miała być gra czasu rzeczywistego i umożliwiać tryb multiplyer musielismy stworzyć wydajny serwer, który pozwoli na wymianę danych pomiędzy graczami. Pomysłów było kilka. Najpierw chcielismy napisać serwer w języku PHP i przechowywać dane o graczach(położenie, cechy postaci, punkty, życie, doswiadczenie itp.) w bazie danych MySQL. Jednak szybko zrezygnowalismy z tej koncepcji, ponieważ obawialismy się, że serwer MySQL nie poradzi sobie z duża iloscia zapytań (24/s od jednego gracza).Rozważalismy też zapis danych do pliku ale przy takim rozwiazaniu musielibysmy się zajać kolejkowaniem połaczeń klientów z serwerem i ustawianiem semaforów aby kilku klientów nie mogło jednoczesnie modyfikować tego samego wpisu w pliku. Poszukujac innych rozwiazań znalezlismy kilka gotowych serwerów(np.SmartFox Server) dla gier typu Real time (implementacja głównie w technologii Java), umożliwiajacych wymianę danych poprzez sockety XML. Postanowilismy stanać na wysokosci zadania i napisać własny serwer na potrzeby wymyslonej przez nas gry. Wiedzielismy, że grafika i częsć logiki będzie wykonana w Flashu. W starszych wersjach flasha do wymiany danych pomiędzy serwerem, a klientem flaszowym można było korzystać z takich funkcji jak loadMovie() loadVariables() XML.load(). Problem polega na tym, że funkcje te nawiazuja połaczenie tylko na czas przesłania danych. Kanał komunikacyjny jest otwierany po wywołaniu jakiegos zdarzenia, potem następuje przesłanie danych i kanał znów jest zamykany. Wysłanie kolejnego żadania do serwera wiaże się z nawiazaniem nowego .Cała ta procedura trwa zbyt długo by umożliwić wymianę danych w czasie rzeczywistym połaczenia i co najważniejsze wykonywanie transakcji klient serwer jest po prostu niemożliwe, gdyż po zamknięciu połaczenia serwer nie jest w stanie skontaktować się z klientem.
Od Flash5 do obsługi trwałego połaczenia TCP/IP między klientem i serwerem oraz wymiany danych np. na potrzeby czatu czy własnie gier sieciowych, służy klasa XMLSocket.
Do wysyłania danych za pomoca obiektu XMLSocket służy metoda XMLSocket.send() należy jednak pamiętać, że wysyła ona dane ASYNCHRONICZNIE w zwiazku z tym moga pojawić się niewielkie opóznienia w przesyłaniu danych na serwer.
Wiedzielismy, że do przesyłania danych klient serwer będziemy korzystać z socketów XMLowych. Zastanawiajac się nad technologia implementacji serwera bralismy ten fakt pod uwagę i szukalismy gotowych komponentów, które choć w niewielkim stopniu ułatwiły by nam oprogramowanie samego mechanizmu nawiazywania połaczeń. Zdecydowalismy się na Delphi6. Do obsługi połaczeń TCP/IP wykorzystalismy klasę TServerSocket, która jest standardowo instalowana w Delphi6, natomiast w nowszych wersjach trzeba doinstalować pakiet dclsockets70.bpl.
Obsługiwanie połaczenia poprzez gniazdka jest bardzo wygodne. Każdy klient komunikuje się z serwerem poprzez swoje prywatne gniazdo, co pozwala na utrzymanie stałego połaczenia klient serwer i nie wymaga ich kolejkowania.
Ogólna koncepcja działania serwera jest następujaca. Serwer nasłuchuje na porcie 10000. Jeżeli na tym porcie pojawi się próba nawiazania połaczenia to zostanie wywołane zdarzenie OnClientConnect. Dla tego zdarzenia uruchamiana jest procedura
procedure TForm1.ServerClientConnect(Sender: TObject; Socket: TCustomWinSocket);
wewnatrz niej przechwytywane sa informacje dotyczace nawiazanych połaczeń, przy użyciu metod jakie udostępnia klasa TServerSocket:
RemoteHost - nazwa hosta, który nawiazał połaczenie RemoteAddress - adres IP hosta ActiveConnection - ilosć aktywnych połaczeń(socketów) ConnectionId - numer identyfikacyjny połaczenia Handle - uchwyt do połaczenia, unikatowy
Wewnatrz wyżej wymienionej procedury tworzony jest kolejny element tablicy rekordów gromadzacych dane o połaczeniu i cechach postaci(tej wybranej przez gracza).Do pól rekordu jest wpisane pierwotne położenie gracza na planszy (poX=280 poY=200 Listing. 2)
Deklaracja rekordu i tablicy rekordów ( Listing 1)
Listing 1
///// rekord zawierajacy informację o : gracz = record idConnection : Integer; //id gracza handle : Integer; // login : String; //loginie klasa : String; poX : Integer; poY : Integer; strzal : Integer; bron_sila : array [0..1] of integer; //tablica przechowujaca numer ataku i współczynnik sily razenia doswiadczenie : Integer; sila : Integer; odpornosc : Integer; zycie : Integer; mana : Integer; pozostale : String; end; Gracze : array of gracz; //tablica rekordow
Dodanie nowego wpisu do tablicy rekordów oraz wypisanie informacji o nowym połaczeniu przedstawiono w Listing 2.
Listing 2.
procedure TForm1.ServerClientConnect(Sender: TObject; Socket: TCustomWinSocket); begin MStan.Lines.Add('Polaczono z '+Socket.RemoteHost+':'+IntToStr(Socket.LocalPort));//Informacja z kim sie połaczono EIloscU.Text:=IntToStr(server.Socket.ActiveConnections);//ilosc aktywnych polaczen (nie bardzo dziala) MKlienci.Lines.Add(Socket.RemoteAddress+' ['+IntToStr(Socket.Handle)+']');//adresy klientow polaczonych SetLength(Gracze,length(Gracze)+1); Gracze[Server.Socket.ActiveConnections-1].idConnection:=Server.Socket.ActiveConnections-1; Gracze[Server.Socket.ActiveConnections-1].handle:=socket.Handle; Gracze[Server.Socket.ActiveConnections-1].poX:=280; Gracze[Server.Socket.ActiveConnections-1].poY:=200; // dla kontroli handle i idConnection //MWysylane.Lines.Add('connection id:'+IntToStr(Gracze[0].idConnection)+'handle:'+IntToStr(Gracze[0].handle)); end;
Serwer jest uruchamiany po wcisnięciu przycisku ON. Procedura obsługujaca to zdarzenie czysci zawartosć pól klasy TMemo i jeżeli serwer nie jest aktywny to uruchamia serwer aktywujac nasłuchiwanie na pobranym z pola TEdit o nazwie EPort porcie, a jeżeli serwer jest aktywny to kończy jego działanie (Listing 3). Posługujemy się tutaj takimi metodami jak
Active - zwraca true jeżeli serwer jest uruchomiony
Open - uruchamia serwer właczajac nasłuchiwanie na wskazanym porcie
Close - wyłacza nasłuchiwanie
Listing 3.
if not server.Active then // wlaczanie begin server.Port:=StrToInt(EPort.Text); //pobranie portu Server.Open; SBStart.Caption:='OFF'; //zmiana napisu na przycisku EIloscU.Text:=IntToStr(server.Socket.ActiveConnections); MStan.Lines.Add('Uruchomiono server...'); end else //i wylaczanie serwera erver.Close;
Kolejne zdarzenie to onClientError, które jest wywoływane w przypadku pojawienia się błędu komunikacji z klientem:
Recive - bład odbioru danych
Send - bład podczas wysywałania danych
Connect - bład podczas nawiazywania połaczenia
Disconnect - bład podczas zamykania połaczenia
Accept - bład podczas akceptowania przychodzacego połaczenia
Jeżeli wystapi którys z wyżej wymienionych błędów to jest on wypisywany w kontrolce TMemo o nazwie MStan (Listnig 4.)
Listinig 4.
procedure TForm1.ServerClientError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); Var bladtxt: string; begin //rodzaje bledow bladtxt:='[ Blad '+IntToStr(ErrorCode)+': '; case ErrorEvent of eeGeneral: bladtxt:=bladtxt+'General ]'; eeSend: bladtxt:=bladtxt+'Send ]'; eeReceive: bladtxt:=bladtxt+'Receive ]'; eeConnect: bladtxt:=bladtxt+'Connect ]'; eeDisconnect: bladtxt:=bladtxt+'Disconnect ]'; eeAccept: bladtxt:=bladtxt+'Accept ]'; end; MStan.Lines.Add(bladtxt); ErrorCode:=0; end;
Kolejne zdarzenie, które należy omówić jest wywoływane w momencie przerwania jakiegos połaczenia OnClientDisconnect. W procedurze obsługi tego zdarzenia
procedure ServerClientDisconnect(Sender: TObject; Socket: TCustomWinSocket);
W polu klasy TMemo o nazwie MStan pojawia się nazwa hosta, z którym przerwano połaczenie oraz ilosć aktywnych połaczeń w polu TEdit o nazwie EIlosc (Listing 5).
Listnig 5.
MStan.Lines.Add('Rozlaczono z '+Socket.RemoteHost+':'+IntToStr(Socket.LocalPort));//Informacja kto sie rozlaczyl EIloscU.Text:=IntToStr(server.Socket.ActiveConnections);
Następnie należy usunać odpowiedni rekord z tablicy Gracze i przesunać wszystkie pozostałe o jeden. Ponadto po przerwaniu połaczenia zmniejszy się własciwosć ConnectionId, należy więc pamiętać by dla wszystkich klientów znajdujacych się za tym, który własnie się rozłaczył zmniejszyć ConnectionId o 1. Na koniec pozostaje tylko zmniejszyć rozmiar dynamicznej tablicy Gracze o 1 (Listing 6).
Listnig 6.
//wycinanieodpowiedniego wpisu w tablicy Gracze i przepisanie indeksow for i := 0 to Length(Gracze)-1 do if Gracze[i].handle = Socket.Handle then break; if i<=Length(Gracze)-2 then //przepisanie graczy na miejsce gracza for j := i to Length(Gracze) - 2 do //się wylogował begin Gracze[j]:=Gracze[j+1]; Gracze[j].idConnection:=Gracze[j].idConnection -1; end; //zmniejszenie rozmiaru tablicy SetLength(Gracze,Length(Gracze)-1);
Teraz pozostało omówić najważniejsze zdarzenie, które jest uruchamiane wtedy gdy na którys z socketów przyjda dane, a mianowicie OnClientRead.
Procedura
procedure TForm1.ServerClientRead(Sender: TObject; Socket: TCustomWinSocket);
obsługujaca to zdarzenie zajmuje się wymiana danych pomiędzy klientami, a serwerem. Jak już wspomniano, w momencie nawiazania połaczenia zwiększana jest ilosć elementów tablicy Gracze i zapisywane sa do niej informacje o nowym połaczeniu. Te informacje to id połaczenia, używane do odsyłania danych, uchwyt (handle), który jest unikatowy dla danego gniazda i służy do rozpoznawania z którego gniazda przyszły dane. Po nawiazaniu połaczenia gracz musi wybrać klasę postaci oraz podać swój login. Te informacje w postaci stringu <#logowanie;login;klasa#> sa wysyłane na serwer. Uruchamia się procedura obsługujaca zdarzenie OnClientRead. Dane przychodzace na serwer sa wpisywane do bufora, który jest dynamiczna tablica znaków. Maksymalny rozmiar bufora musi być równy maksymalnej ilosci danych, które moga przyjsć w jednym pakiecie, czyli 4096 bajtów. Metoda Socket.ReciveLength() zwraca rozmiar odebranych danych, natomiast metoda Socket.ReceiveBuf() służy do przepisania danych, odebranych na sockecie, do bufora (Listnig 7).
Listnig 7.
var buforek: array[0..4095] of char ; //deklaracja tablicy znakow (bufor) begin //przypisanie danych do bufora Socket.ReceiveBuf(Buforek,socket.ReceiveLength);
Teraz swoje działanie rozpoczyna funkcja zajmujaca się wyodrębnieniem komunikatów z bufora czyli ciagu znaków spomiędzy znaczników <# #>.Każdy z komunikatów zapisywany jest w kolejnym elemencie tablicy dynamicznej.
Teraz omówimy działanie funkcji wyodrębniajacej komunikaty z bufora
Function RozbijPakiet(pakiet: string; pocz, kon: string):DTS;
Jak widać funkcja przyjmuje dane w postaci strigu. Pakiet to zawartosć bufora, pocz i kon to dwuelementowy string stanowiacy prefix i sufix komunikatu. Pomiędzy tymi znacznikami znajduje się poszukiwany komunikat. Celem działania funkcji jest wszystkich znaków znajdujacych się pomiędzy <# #> i wpisanie ich do dynamicznej tablicy stringów DTS. DTS to zdefiniowany przez nas typ na potrzeby tej własnie funkcji
DTS = array of string; //Dynamiczna Tablica Stringow
Kod funkcji został przedstawiony w listingu nr. 8
Listnig 8.
Function TForm1.RozbijPakiet(pakiet: string; pocz, kon: string):DTS; //pocz i kon musza mieć rozmiar inaczej się posypie var i,k: integer; f: boolean; begin i:=-1; f:=false; setlength(result,0); for k:=1 to length(pakiet)-2 do if pakiet[k-1]+pakiet[k]=pocz then begin f:=true; inc(i); SetLength(result,length(result)+1); end else if pakiet[k]+pakiet[k+1]=kon then begin f:=false; end else if f then result[i]:=result[i]+pakiet[k]; end;
Używana tutaj zmienna i iteruje po elementach zwracanej tablicy stringów, zmienna k iteruje po znakach zmiennej pakiet (zawartosć bufora). Dopisywanie znaków do rekordu w tablicy następuje tylko wtedy gdy flaga f jest ustawiona na true. Z kolei flaga jest ustawiana na true wtedy gdy w stringu wejsciowym zostanie rozpoznanay prefix zaszyty w zmiennej pocz.Rozpoznanie prefixu prowadzi do zwiększenia rozmiaru zwracanej tablicy i inkrementacji zmiennej i. Koniec wpisywania następuje, gdy zostanie rozpoznany sufix zaszyty w zmiennej kon.
Dane zwrócone przez funkcję RozbijPakiet() sa zapisywane do tablicy dynamicznej dane. Teraz dla każdego elementu tablicy dane musi zostać wywołana funkcja, która rozbije komunikaty na nagłówek i pola, zawierajace informacje o stanie gracza badz o tresci informacji przesyłanej czatem. Ta Funkcja to
function RozbijString(napis: string; separator: char):DTS;
Dane wejsciowe to napis zawierajacy tresć pojedynczego komunikatu, separator to pojedynczy znak oddzielajacy pola komunikatu. Funkcja zwraca dane w postaci dynamicznej tablicy stringów, do kolejnych elementów tej tablicy sa wpisywane wyodrębnione z komunikatu pola. Kod funkcji reprezentuje Listnig.9
Listnig.9
function TForm1.RozbijString(napis: string; separator: char):DTS; var i,k: integer; begin i:=0; SetLength(result,1); for k:=1 to length(napis) do if napis[k]=separator then begin inc(i); SetLength(result,length(result)+1); end else begin result[i]:=result[i]+napis[k]; end; end;
Zmienna k iteruje po znakach komunikatu napis jeżeli natrafi na znak podany w zmiennej separator to wtedy ma zwiększyć zmienna i a tym samym rozmiar zwracanej tablicy result. Jeżeli odczytywany jest inny znak niż separatorto ma on być dopisywany do zwracanej tablicy znaków result pod jej aktualny indeks i.
Częsć kodu z procedury obsługi zdarzenia OnClientRead() zawierajaca pętle rozbijania komunikatów z tablicy dane na pola zapisywane do tablicy tabS (Listing. 10).
Listing. 10
dane: DTS; tabS: DTS; silaA: integer; //bufR: integer; begin Socket.ReceiveBuf(Buforek,socket.ReceiveLength); dane:=RozbijPakiet(Buforek,'<#','#>'); //zliczanie komunikatow z memo //ELIczMemo.text:=IntToStr(strToInt(ELiczMemo.text)+1); for l := 0 to length(dane) - 1 do // begin tabS:=RozbijString(dane[l],';');
W kodzie procedury odczytu danych z socketów znajduja się juz tylko warunki stanowiace o obłsudze rodzaju komunikatu. Jak już wspomniano rodzaj komunikatu jest rozpoznawany przez nagłówek, który znajduje się w tab[0]. Rodzaje komunikatów:
gracz_wiadomosc;trescWiadomosci;doKogo(login) - obsługa czatu pomiędzy graczami
logowanie;login;klasa - obsługa logowania
dane_gracza;poX;poY;Strzal - obsługa porusznia się postaci po planszy i informacja o tym czy gracz wystrzelił
trafiono;kogo(login);numer_ataku - obsługa walki, trafienie, ujmowanie życia
Jeżeli zostanie rozpoznany nagłówek dane_gracza to należy do odpowiedniego rekordu dodać współrzędne poX, poY i informację o strzale. Wyszukiwanie tego odpowiedniego elementu w tablicy rekordów Gracze[] odbywa się przez porównywanie uchwytu aktualnego połaczenia socket.Handle i uchwytów wpisanych w polu rekordu Gracze[i].handle.(Listing 11)
Listing 11
if tabS[0]='dane_gracza' then begin //sprawdz od ktorego to gracza dostałes for i := 0 to Length(Gracze) - 1 do if Gracze[i].handle=socket.Handle then break; //wpisz dane do tablicy w odpowiednie rekordy Gracze[i].poX:=StrToInt(tabS[1]); Gracze[i].poY:=StrToInt(tabS[2]); Gracze[i].strzal:=StrToInt(tabS[3]);
Dane o zmianie położenia gracza przesyłajacego komunikat maja trafić do wszystkich pozostałych graczy. Tak więc należy rozesłać dane do tych wszystkich graczy, których uchwyt jest różny od aktywnego(tego z którego przyszedł komuikat).Ponad to dane sa rozsyłane jedynie do tych graczy, którzy znajduja się w polu widzenia postaci, która my się poruszamy (kwadrat 500x500 px) (Listing 12)
Listing 12
//przejdz po tablicy Gracze i sprawdz isteniejacych graczy for j := 0 to Length(Gracze) - 1 do begin //jezeli trafiles na gracza ktory teraz przeslal dane to idz dalej if Gracze[j].handle = socket.Handle then continue; //kwadrat 500 500 //oblicz odleglosc miedzy graczami, przeslij dane jesli sa w polu widzenia if (Gracze[j].poX - Gracze[i].poX < 275) AND (Gracze[j].poY - Gracze[i].poY < 275) then begin MWysylane.Lines.Add('wysyłam ten strzal'); wyslij_do(Gracze[i].login+';'+IntToStr(Gracze[i].poX)+';'+IntToStr(Gracze[i].poY)+';'+IntToStr(Gracze[i].strzal),0,Gracze[j].login); end; end;
Wysyłany string ma postać login;poX;poY;strzał. Wysłaniem danych do konkretnego gracza zajmuje się precedura wyslij_do()
procedure wyslij_do (info:String;do_kogo:String);
Znaczenie zmiennych wejsciowych:
Info - zmienna typu string przechowujaca komunikat do wysłania
Do_kogo - zmianna typu string przechowujaca login adresat
Procedura jest wykorzystywana do rozsyłania informacji o trafieniach, podczas wymiany informacji pomiędzy graczami(czat) oraz przy przesyłaniu informacji o współrzędnych, ale tylko do tych graczy którzy sa w polu widzenia nadawcy.
Kod procedury jest przedstawiony w listingu. 13
Listing. 13
procedure TForm1.wyslij_do (info:String; do_kogo:String); var i:Integer; begin //przeslij do gracza o podanym loginie info + login nadawcy for i := 0 to length(Gracze) - 1 do begin if Gracze[i].login=do_kogo then begin Server.Socket.Connections[Gracze[i].idConnection].SendText(info+#0); MWysylane.Lines.Add('wysylam do: '+Gracze[i].login+' tekst: '+info); break; end; end; end;
Procedura korzysta z zmiennej lokalnej: i używanej do iteracji po tablicy Gracze[]. W pętli for następuje przeszukanie tablicy Gracze[]. Jeżeli natrafimy na element tablicy, który zawiera taki sam login jak login nadawcy to pod numer idConnection (zapisany w rekordzie dla tego gracza) wysyłamy komunikat posługujac się metoda SendText()
Server.Socket.Connections[Gracze[i].idConnection].SendText(info+#0) Server.Socket.Connection[numer id połaczenia odczytany z rekordu] SendText(tresć wiadomosci)
Należy wspomnieć, że tresć wiadomosci składa się z przekazanej informacji i znaku końca stringu (znak o kodzie 0 w ASCII).
Kolejny rodzaj komunikatu to:
gracz_wiadomosc;trescWiadomosci;doKogo(login)
Jest on wykorzystywany do komunikowania się graczy (czat), odsyłana do adresata informacja jest umieszczona w komunikacie postaci gracz_wiadomosc;tresćWiadomosci;login_nadawcy
Uzyskaniem loginu nadawcy zajmuje się funkcja login_dla_handle(). Przeszukuje ona pola rekordu gracz umieszczonych w tablicy Gracze[] . Jeżeli uchwyt dla aktywnego socketu (tego z którego własnie odczytano komunikat) jest taki jak uchwyt zapisany w polu rekordu o nazwie handle to znaczy, że pole login zawiera poszukiwany przez nas login.
Kod funkcji login_dla_handle() (Listing. 14)
/////////////////////////////////////////////////////////////////////////////// ///Funkcja zwracajaca login na podstawie danej socket.handle charakterystycznej ///dla danego połaczenia...korzysstamy z tablicy rekordów Gracze[] function TForm1.login_dla_handle(handle_od_kogo : Integer):String ; var i : Integer; zwroc_login :String; begin for i:=0 to length(Gracze)-1 do begin if Gracze[i].handle=handle_od_kogo then begin zwroc_login:=Gracze[i].login; break; end; end; Result:=zwroc_login; end;
Majac już login nadawcy i wyodrębniona z przychodzacego komunikatu tresć wiadomosci można na ja przesłać do odpowiedniego gracza za pomoca opisywanej już funkcji wyslij_do(), a poniżej fragment kodu zajmujacy się rozsyłaniem widomosci pomiędzy graczami
Listing 15
...
if tabS[0]='gracz_wiadomosc' then
begin
//dowiedz sie od kogo(login nadawcy) ta wiadomosc
login_nadawcy:=login_dla_handle(socket.Handle);
wyslij_do('gracz_wiadomosc;'+tabS[1]+';'+login_nadawcy,tabS[2]);
end else
...
Kolejny komunikat pojawia się podczas logowania nowego gracza.
logowanie;login;klasa
Podczas logowania musimy pamiętać o tym aby sprawdzić czy podany login jest zajęty. Jeżeli nie jest zajęty to należy go dopisać do odpowiedniego rekordu w pole login, nowo zalogowanemu graczowi przesłać cechy postaci ,która wybrał, loginy klasy i pozycje graczy już znajdujacych się na mapie. Natomiast wszystkich pozostałych graczy należy poinformować o fakcie pojawienia się nowego gracza.
Poniżej przedstawiamy rozpoznanie nagłówka komunikatu o logowaniu oraz sposób sprawdzenia czy login jest zajęty Listing 16
Listing 16
if tabS[0]='logowanie' then begin login_zajety:=0; for i := 0 to length(Gracze)-1 do if Gracze[i].login = tabS[1] then begin login_zajety:=1; break; end; // MWysylane.Lines.Add('Wydzielilem podstring logowanie?; '+tabS[0]+' login zajety:'+IntToStr(login_zajety)); if login_zajety = 1 then begin socket.SendText('blad_logowania;Ten login: '+tabS[1]+' jest zajety'); end else //doadaj login dla nowego gracza begin
W pętli for przeszukiwana jest tablica rekordów Gracze[] jeżeli wpis w polu login zgadza się z loginem gracza, który próbuje się zalogować to wartosć zmiennej login_zajety jest ustawiana na 1 a pętla jest przerywana. Po zakończeniu działania pętli spradzana jest wartosć flagi login_zajety jeżeli jest to 1 to do graca odsyłany jest komunikat blad_logowania;Ten login: podany login jest zajety. Po otrzymaniu takiego komunikatu klient zrywa połaczenie. Obsługa zerwanego połaczenie zajmuje się już wczesniej omawiana procedura ServerDisconnect.
Natomiast jeżeli flaga login_zajety jest ustawiona na 0 to przeszukiwana jest tablica rekordów Gracze[] (porównywanie uchwytów) w celu odnalezienia rekordu, który ma być uzupełniony o takie dane jak login I klasa postaci. Ponad to w zależnosci od wybranej klasy do rekordu trafiaja cechy wybranej postaći takie jak doswiadczenie, mana, rodzaje broni, siła.
Listing 17
else //doadaj login dla nowego gracza
begin //dla nowego polaczenia
for i:=0 to Length(Gracze) do
begin
MWysylane.Lines.Add('dodano nowy login'+Gracze[i].login+' przed petla z handle');
MWysylane.Lines.Add('handle z Gracze[]:'+IntToStr(Gracze[i].handle)+' handle z socket'+IntToStr(socket.Handle));
if Gracze[i].handle = Socket.Handle then
begin
Gracze[i].login:=tabS[1];
Gracze[i].klasa:=tabS[2];
//wpisanie charakterystyk dla graczy
if tabS[2]='wojownik' then
begin
Gracze[i].sila:=100;
Gracze[i].odpornosc:=100;
Gracze[i].zycie:=200;
Gracze[i].mana:=0;
Gracze[i].doswiadczenie:=0;
Gracze[i].bron_sila[0]:=12;
Gracze[i].bron_sila[1]:=32;
end
else
if tabS[2]='lucznik' then
begin
Gracze[i].sila:=70;
Gracze[i].odpornosc:=70;
Gracze[i].zycie:=150;
Gracze[i].mana:=0; //czy ma to być licznik strzał??
Gracze[i].doswiadczenie:=0;
Gracze[i].bron_sila[0]:=22;
Gracze[i].bron_sila[1]:=42;
end
else //czarodziej
begin
Gracze[i].sila:=50;
Gracze[i].odpornosc:=50;
Gracze[i].zycie:=120;
Gracze[i].mana:=100;
Gracze[i].doswiadczenie:=0;
Gracze[i].bron_sila[0]:=7;
Gracze[i].bron_sila[1]:=37;
end;
Dodane cechy musza być odesłane do klienta, by ten wiedział jaka postać załadować i jakie sa jej charakterystyki. Odsyłany jest więc komunikat dane_gracza;sila;odpornosc;zycie;mana;doswiadczenie , który musi byc zakończony znakiem końca linii
socket.SendText('dane_gracza;'+IntToStr(Gracze[i].sila)+';'+IntToStr(Gracze[i].odpornosc)+';'+IntToStr(Gracze[i].zycie)+';'+IntToStr(Gracze[i].mana)+';'+IntToStr(Gracze[i].doswiadczenie)+#0);
Teraz pozostaje jeszcze przeszukać tablice rekordów Gracze[] i do nowo zalogowanego gracza przesłać komunikaty które poinformuja go o postaciach znajdujacych się na mapie. Odsyłane sa komunikaty dwóch rodzajów:
Nowy_gracz;login;klasa
Login;poX;poY;Strzal
Takie same komunikaty ale dotyczace nowo zalogowanego gracza musza trafić do wszystkich juz istniejacych graczy, tym rozsyłaniem zajmuje się procedura rozeslij()
Poniżej kod obsługujacy rozsyłanie wyżej wymienionych komunikatów do nowego gracza I do graczy juz zalogowanych
Listing. 18
if Length(Gracze)>1 then begin //logujacy sie gracz dostaje tyle stringow ilu jest zalogowanych graczy - on sam for j := 0 to Length(Gracze) - 1 do begin if Gracze[j].login=Gracze[i].login then continue; socket.SendText('nowy_gracz;'+Gracze[j].login+';'+Gracze[j].klasa+#0); socket.SendText(Gracze[j].login+';'+IntToStr(Gracze[j].poX)+';'+IntToStr(Gracze[j].poY)+';'+IntToStr(Gracze[j].Strzal)+#0); end; //starzy gracze dostaja info o nowym playerze // każdy z nich dostaje string: nowy_gracz;login_nowy;klasa_nowy rozeslij('nowy_gracz;'+Gracze[i].login+';'+Gracze[i].klasa,Gracze[i].handle); // następnie string login;pozX;pozY;strzał rozeslij(Gracze[i].login+';'+IntToStr(Gracze[i].poX)+';'+IntToStr(Gracze[i].poY)+';'+IntToStr(Gracze[i].Strzal),Gracze[i].handle); end;
Teraz kilka słów o precedurze rozeslij(). Jej zadaniem jest rozesłanie komunikatu do wszystkich graczy oprócz samego nadawcy.
Listing 19
procedure TForm1.rozeslij (info:String;od_kogo:Integer); var i:Integer; begin for i := 0 to Length(Gracze) - 1 do begin if Gracze[i].handle=od_kogo then continue; Server.Socket.Connections[Gracze[i].idConnection].SendText(info+#0); end; MWysylane.Lines.Add('rozsyłam oprócz'+IntToStr(od_kogo)+' wiadomosc: '+info); end;
Procedura jako parametry wejsciowe przyjmuje info, czyli komunikat do wysłania oraz numer uchwytu (handle) nadawcy komunikatu przekazany w parametrze od_kogo. Procedura przeszukuje tablice rekordów Gracze[] i sprawdza czy wpis w polu handle zgadza się z numerem argumentu od_kogo jeżeli tak to rekord ten jest pomijany, a jeżeli nie to pod numer połaczenia przechowywany w polu rekordu o nazwie idConnection jest wysyłany komunikat o tresci zgodnej z info , przy użyciu metody SendText().
Ostatni z komunikatów trafienie;kogo(login);numer_ataku jest wysyłany przez klienta flashiwego, gdy którys z graczy zostanie trafiony. Serwer zajmuje się obliczaniem ilosci życia jakie zostało graczowi po zadaniu ciosu. Wpływ na obrażenia ma odpornosć poszkodowanego, siła i rodzaj broni postaci zadajacej cios. Najmniejsza wartosć obrażeń to 5.
W rekordzie gracz znajduje się tablica statyczna (bron_gracz[]) przechowujaca współczynnik rażenia broni jakimi dysponuje dany gracz. Po obliczeniu ile życia pozostało, do gracza,który został trafiony jest wysyłany komunikat postaci zostalem_trafiony;ilosć_zycia. Wiadomosć jest przesyłana funkcja wyslij_do(). W sytuacji gdy gracz straci życie jest przesyłany komunikat zostalem_trafiony;0 , gdzie 0 - to stan życia. Flash reaguje w ten sposób, że postać odradza się w obszarze zdemilitaryzowanym, natomiast serwer zachowuje wszystkie cechy postaci i przydziela postaci pełne życie postać się odradza Listing 20.
Listing 20
/////////////////////////////////////////////////////////////////////////// /// Obsługa trafien i ujmowania zycia /// jezeli ktos kogos trafi to flash wysyla string trafienie;kogo(login);numer_ataku /// serwer oblicza zycie i przesyla string do trafionego zostalem _trafiony;aktualny_poziom_zycia
if tabS[0]='trafienie' then begin for i := 0 to Length(Gracze) - 1 do if Gracze[i].handle = Socket.Handle then //kto trafil begin for j := 0 to Length(Gracze) - 1 do if Gracze[j].login = tabS[1] then //kogo trafiono begin silaA:=Gracze[i].bron_sila[StrToInt(tabS[2])]-gracze[j].odpornosc; if silaA<5 then silaA:=5; //najmniejsza wartosc ataku to 5 Gracze[j].zycie:=Gracze[j].zycie -silaA; if Gracze[j].zycie<=0 then begin //przywrocenie zycia po restarcie if Gracze[j].klasa='wojownik' then Gracze[j].zycie:=200 else if gracze[j].klasa='lucznik' then Gracze[j].zycie:=150 else Gracze[j].zycie:=120; //przesylam zycie 0 co oznacza smierc wyslij_do('zostalem_trafiony;'+IntToStr(0)+#0,Gracze[j].login); MWysylane.Lines.Add('zostalem_trafiony;'+IntToStr(0)+#0); //restart wyslij_do('zostalem_trafiony;'+IntToStr(gracze[j].zycie)+#0,Gracze[j].login); MWysylane.Lines.Add('zostalem_trafiony;'+IntToStr(gracze[j].zycie)+#0); end else begin //socket.SendText('zostalem_trafiony;'+IntToStr(Gracze[j].zycie)+#0); wyslij_do('zostalem_trafiony;'+IntToStr(Gracze[j].zycie)+#0,Gracze[j].login); MWysylane.Lines.Add('zostalem_trafiony;'+IntToStr(Gracze[j].zycie)+#0); end; break; end; break; end;
Jak już wspomnielismy na samym poczatku flsh komunikuje się z serwerem używajac obiektu klasy XMLSocket. Nie znaczy to jednak, że przesyłamy dane w formacie XML. Oczywiscie zastanawialismy się nad takim rozwiazaniem ale wysyłanie i odbieranie danych zapisanych w XML wiaże się z ich parsowaniem co tylko zmniejszało by wydajnosć serwera i wydłużało proces wymiany danych pomiędzy graczami. Do wydzielania komunikatów z strumienia danych przesułanych w datagramie użylismy własnych znaczników <# #>.
Obciażenie komputera na którym jest uruchomiony serwer zwiększa wyswietlenie danych które sa odbierane i wysyłane. Monitorowanie tego co dzieje się na serwerze pozwoliło nam na wychwytywanie jego ewentualnych błędów. Aby nie obciażać serwera wyswietlaniem przesy łanych informacji, gdy to nie jest konieczne, utworzylismy przycisk właczania i wyłacznaia tej funkcji MONITORUJ
Aktualna statystyka wszystkich graczy jest zapisywana do kontrolki typu StringGrid. Po kliknięciu przycisku AKTUALIZUJ dane sa aktualizowane.
Bezposrednio z serwera można wysłać komunikat do klienta o wybranym idConnection
Caly mechanizm gry zostal napisany we Flashu. Zabierajac sie do tego projektu, nie wiedzielismy, jakimi mechanizmami sluzacymi do laczenia sie flasha z programem (tu serwerem) napisanym w innym jezyku programowania bedziemy sie poslugiwac. Tak wiec sprawa lacznosci klienta z serwerem byla dla nas czyms calkowicie nowym.
Chcielismy, aby serwer odpowiadal za obliczanie bardziej zlozonych elementow naszej gry, tym samym odciazajac klienta. Natomiast klient napisany we Flashu mial odpowiadac za mechanizm gry (tu reguly w niej panujace, interakcje uzytkownika, oraz grafike). Nie mozna tu zapomniec, ze musielismy stworzyc odpowiedni obiekt odpowiadajacy za odczytanie, formatowanie i rozeslanie do konkretnych elementow danych przychodzacych z serwera.
Z zalozen pierwotnej wersji naszej gry wynikalo, ze gracz ma mozliwosc wybrania imienia swojego gracza, wyboru klasy swojej postaci. Nastepnie gracz zostalby przeniesiony do poczekalni skad dopiero po zalogowaniu sie wszystkich graczy (w pierwszej wersji gry mialo ich byc tylko czterech)i kliknieciu przez nich przycisku START, mieli oni zostac przeniesieni na mape gry, gdzie miala sie toczyc rozgrywka.
Jezeli chodzi o sama gre, rozwazalismy rozne pomysly poczawszy od umiejscowieniu jej w czasie az po grafike. Ustalilismy, ze gracz bedzie poruszal jedna postacia, za pomoca klawiatury. Postac moze poruszac sie w osmiu kierunkach.
Aby zwiekszyc obszar planszy, zdecydowalismy sie na dosc kontrowersyjne rozwiazanie, aczkolwiek skuteczne. Zalozylismy, ze nasz gracz bedzie stal w miejscu natomiast podczas korzystania z klawiszy strzalek na klawiaturze poruszac miala sie mapa, na ktorej znajdowal sie gracz. Dzieki temu rozmiar mapy zwiekszylismy do maksymalnych rozmiarow, na jakie pozwalal program Flash. Czyli podczas trzymania strzalki, poruszala sie mapa natomiast gracz stal w miejscu wykonujac jedynie obrot w kierunku przeciwnym do ruchu mapy. Zastosowanie tego rozwiazania w znacznym stopniu poprawilo jakosc gry utrudniajac tym samym obliczanie polozenia graczy na mapie. Poniewaz mapa nie byla statyczna musielismy wykonywac sporo obliczen, aby podac wspolrzedne gracza znajdujacego sie na mapie.
Problem pojawil sie rowniez w przypadku obiektow znajdujacych sie na mapie, kazdy obiekt musial podczas jego zaladowania zawierac kod odpowiadajacy za jego stale przesuwanie podczas ruchu mapy. Paradoks polega tutaj na tym, ze aby uzytkownikowi wydawalo sie, ze obiekty sie nie przemieszczaja, tylko przemieszcza sie sam gracz wzgledem nich, kazdy obiekt musial sie poruszac z taka sama predkoscia i w tym samym kierunku co cala mapa. Kod umieszczany w obiektach znajdujacych sie na mapie wyglada nastepujaco:
onClipEvent(load)
{
pox = _x; // pobieranie wspolrzednych poczatkowych podczas zaladowania obiektu
poy = _y;
}
onClipEvent(enterFrame)
{
// w kazdej ramce do wspolrzednych poczatkowych niezmieniajacych sie dodajemy wspolrzedne mapy.
_x = _root.map._x + pox;
_y = _root.map._y + poy;
}
Tak wiec po wykonaniu tych operacji mielismy jedna postac (jak na razie byl to tylko trojkat, tak aby w latwy sposob moc zauwazyc, czy odpowiednio zmienia swoj kierunek w stosunku do poruszajacej sie planszy) ktora mozemy poruszac po planszy.
Chcielismy, aby nasza mapa stwarzala pewne ograniczenia dla gracza np. aby nie mogl on przechodzic przez gory albo inne przeszkody.
We Flashu znajduje sie funkcja MovieClip.hitTest(x,y,target). Jej zadaniem jest sprawdzenie, czy dwa obiekty w danym czasie nie nakladaja sie, jezeli tak to zwracana jest wartosc true, w przeciwnym wypadku false.
Sprawdzalismy, z ktorej strony gracz wszedl na obiekt i w przypadku kolizji odsuwalismy go w strone przeciwna. Zastosowalismy ten model w podstawowej wersji gry, jednak nastapily pewne komplikacje, mianowicie gra byla odswiezana 24 razy na sekunde, zdazaly sie takie przypadki, w ktorych dwa obiekty byly bardzo blisko siebie, ale nie nastepowala kolizja. W chwili, gdy gracz odwrocil postac, zaczal dzialac hitTest, czego wynikiem byly sytuacje, w ktorych odwracajac gracza przy skale nastepowalo jego przemieszczenie przez caly obszar skaly na druga strone.
Kolejnym pomyslem bylo zapisanie wspolrzednych wszystkich obiektow znajdujacych sie na planszy w wielkiej tablicy znajdujacej sie po stronie serwera. Zadaniem serwera byloby sprawdzenie czy gracz nie kiliduje ze wspolrzednymi zapisanymi w tablicy.
Plusem tego rozwiazania byloby odciazenie Flasha, ktorego mapa zawieralaby jedynie obraz planszy a nie osobne obiekty, ktore oczywiscie trzeba by bylo przesuwac rowno z sama mapa. Dodatkowo nie trzeba by bylo wykonywac hitTestow po stronie klienta i jednoznacznie okreslic czy gracz koliduje z jakims obiektem, czy nie.
Natomiast minusem takiego rozwiazania byloby problematyczne wpisywanie wspolrzednych obiektow do tablicy, ciagle, wielokrotne przechodzenie tej tablicy przez serwer, ktory po pierwsze musialby uaktualniac dane, a nastepnie przegladac tablice 24 razy na sekunde dla kazdego gracza. W dodatku kazdy gracz ruszalby siesie dopiero po otrzymaniu pozwolenia z serwera, co moglo, by pogorszyc stan rozgrywki.
Ostatecznym pomyslem moze nie najlepszym, ale za to skutecznym i nieobciazajacym znacznie serwera jest, wykonywanie hitTestow po stronie klienta, w dosc specyficzyn sposob. Na mapie zostaly narysowane obiekty, dzieki temu staly sie jej integralna czescia, tak jak zwykla bitmapa. Nastepnie na obrzezach narysowanych obietkow zostaly umieszczone linie. Kazda linia ma przypisany swoj identyfikator i okresla, z ktorej strony stanowi ograniczenie dla gracza. Ponizszy rysunek najlepiej zobrazuje to dzialanie:
W grze linie sa o wartstwe nizej od mapy dlatego ich nie widac. Rowniez one maja zapisany kod odpowiadajacy za ich przesowanie sie razem z mapa. Jak widac na obrazku kazda linia ma swoj identyfikator.
Poruszajacy sie po planszy gracz przeglada wszystkie linie za pomoca funkcji for sprawdzajac, czy przypadkiem na ktorejs z nich nie jest spelniony warunek hitTest. Nie musimy sprawdzac kierunku gracza, poniewaz linia ewidentnie nam wskazuje ten kierunek.
Dzieki temu nie mamy sytuacji, w ktorej gracz zostaje przepychany przez obiekt. Obawialismy sie, ze Flash nie bedzie w stanie efektywnie przechodzc przez duza ilosc lini sprawdzajac nasz warunek, jednak dla 80 linii efekt jest natychmiastowy. Ponizej zostal przedstawiony kod sprawdzajacy czy gracz koliduje z ktoras z lini:
onClipEvent(enterFrame)
{
for (i=0; i >;_root.postac.il_blk; i++)
{
scianaL = "_root.linL"+i;
scianaP = "_root.linP"+i;
scianaG = "_root.linG"+i;
scianaD = "_root.linD"+i;
if (this, hitTest(scianaL))
_root.map._x += 10;
else if (this, hitTest(scianaP))
_root.map._x -= 10;
else if (this, hitTest(scianaG))
_root.map._y += 10;
else if (this, hitTest(scianaD))
_root.map._y -= 10;
}
}
Jak widac jest on bardzo prosty i nie wymaga chyba komentarza. Teraz nasz gracz moze juz poruszac sie po planszy i kolidowac z umieszczonymi na niej obiektami.
Kolejnym krokiem bylo stworzenie potencjalnemu uzytkownikowi mozliwosci wyboru klasy postaci. W tym celu musielismy stworzyc menu pozwalajace na dokonanie wyboru i tym samym pozwalajace na zapisanie swojej nazwy, ktora z kolei bedzie wyswietlana pod graczem.
Jezeli chodzi o wyswietlanie nazwy pod graczem, to jest to bardzo latwe do wykonania,pamietamy, ze gracz glowny sie nie przemieszcza wzgledem ekranu dlatego tez mozna bylo na stale umiescic pod nim Dynamic Text, do ktorego zaraz po zalogowaniu zostawala przypisywana tresc loginu.
Samo menu znajdowalo sie w pierwszej klatce gry, umozliwialo dokonanie wyboru jednej z trzech klas postaci, oczywiscie na poczatku byl to jedynie kwadrat, trojkat, kolko. Dzieki zastosowaniu we Flashu biblioteki tworzonych obiektow, w finalnej wersji gry wystarczylo zmienic tylko grafike bazowego obiektu, a zostala ona uwzgledniona we wszystkich klatkach gry.
Pierwsza wersja gry umozliwiala tylko podanie loginu gracza, natomiast adres serwera, z ktorym laczyl sie gracz byl wkompilowany w plik wynikowy. Powodowalo to pewne niedogodnosci, poniewaz podczas testowania naszej gry wielokrotnie zmienialismy sieci i za kazdym razem zmuszalo nas to do przekompilowania pliku zrodlowego. Dlatego tez w koncowej wersji mamy mozliwosc wpisania adresu serwera w menu na samym poczatku gry, ale sprawe polaczenia omowimy pozniej.
Stworzylismy osobny obiekt POSTAC, ktorego zadaniem bylo gromadzenie wszystkich informacji o graczach, zarowno glownym, jak i tych, ktorzy uczestnicza w rozgrywce. Zaraz po zalogowaniu dane naszego gracza zostaja umieszczone w tym obiekcie. Z poczatku korzystalismy ze zwyczajnych zmiennych jednak potrzeba przystosowania naszej gry do wiekszej liczby uzytkownikow, tudziez mozliwosc rozbudowy w przyszlosci zmusila nas do zastosowania tablic.
Dlatego tez w obiekcie POSTAC znajduja sie dwie tablice jedna tab_nazw zawierajaca wszystkie loginy graczy uczestniczacych w rozgrywce lacznie z graczem glownym, przy zalozeniu ze gracz glowny zawsze jest wpisywany do indeksu zerowego tablicy, oraz druga tab_klas, zawierajaca spis klas graczy.
Po wyborze klasy gracza wpisaniu jego nazwy, podaniu adresu serwera, gra zaczyna dzialac przenoszac sie do klatki o nazwie USTAWIENIA (jest to dzialanie bez wiedzy uzytkownika). Tutaj nastepuje skopiowanie odpowiedniego wzorca klasy postaci, nadanie mu nazwy ustawieniu na pozycji bazowej, oraz skasowaniu oryginalow, tak aby nie stwarzaly problemow podczas gry. Czyli jezeli wybierzemy klase CZARODZIEJ program skopiuje obiekt o nazwie CZARODZIEJ, oraz przypisze mu odpowiednie dane. Nastepnie skasuje orginalne obiekty pozostalych klas, lacznie z orginalem obiektu czarodziej.
Dzialanie to jest wynikiem tego, ze kazdy obiekt we Flashu musi miec unikalna nazwe (lub nie posiadac jej wcale). Dlatego jezeli chcemy miec obiekt o nazwie gracz0, niezaleznie od tego jaka klase gracza wybierzemy, musimy wykorzystac nastepujacy zapis :
if(_root.postac.tab_klas[0]=="czarodziej")
{
duplicateMovieClip(_root.czar,"gracz0",1);
}
else if(_root.postac.tab_klas[0]=="wojownik")
{
duplicateMovieClip(_root.wojo,"gracz0",1);
}
else if(_root.postac.tab_klas[0]=="lucznik")
{
duplicateMovieClip(_root.lucz,"gracz0",1);
}
//polozenie poczatkowe gracza
_root.gracz0._x=280;
_root.gracz0._y=200;
//kasowanie orginalow
unloadMovie(_root.czar);
unloadMovie(_root.wojo);
unloadMovie(_root.lucz);
Czyli sprawdzamy, jaka klase wybral uztkownik, nastepnie duplikujemy obiekt o konkretnej nazwie klasy, zapisujac go pod nazwa gracz0. Kolejnym krokiem jest ustawienie gracza na pozycji bazowej, ustalonej mniej wiecej na srodku ekranu i skasowanie orginalow obiektu gracza.
Dzieki temu na planszy pojawi nam sie taki obiekt graficzny, jaka klase wybralismy, czyli dla czarodzieja kolko, dla lucznika trojkat, a dla wojownika kwadrat.
To byla ta latwiejsza czesc mechanizmu gry po stronie klienta. Nalezy tu jeszcze wspomniec, ze bardzo wazne jest przejrzyste zapisywanie wszystkich danych, wielokrotnie zdazylo nam sie, ze np nakladaly sie ramki, obiekty wystepowaly na tych samych warstwach itp. Dlatego warto dobrze rozplanowac umiejscawianie kodu, jednoznacznie okreslac zmienne, dzieki temu unikniemy wielu bledow. Sa to bledy drobne jednak odnalezienie ich jest bardzo trudne, szczegolnie w tak duzej ilosci kodu w dodatku bardzo rozproszonego, poniewaz Flash umozliwia programowanie kazdego obiektu z osobna i w dodatku w kazdej klatce inaczej.
Teraz postaramy sie opisac jak wyglada polaczenie naszego klienta z serwerem. Serwer zostal napisany w Delphi, z poczatku miala to byc baza MySQL przechowujaca rekordy, do ktorych mieliby dostep wszyscy gracze, jednak byl to sposob stosunkowo wolny a w dodatku wymuszal przeladowanie strony po stronie klienta, co calkowicie mijalo sie z celem.
Nastepnie zastanawialismy sie nad gotowymi serwerami takimi jak Smart Fox Server. Jednak byly to malo ambitne jak na nas zalozenia.
Pierwsze polaczenie z serwerem napisanym w Delphi bylo to tylko wyslanie wiadomosci witaj, ktora nastepnie byla odsylana do nadawcy. Niby nic, ale pokazalo nam, w jaki sposob nawiazywac polaczenie, przesylac i odbierac dane z serwera.
Musielismy utworzyc osobny obiekt odpowiedzialny za ustalenie polaczenia. Sam kod polaczenia z serwerem mozna wydzielic na kilka etapow pierwszym jest utworzenie obiektu XML Socket, nastepnie proba polaczenia z serwerem na porcie 10000. Jezeli polaczenie wystapilo bezblednie debuger otrzymywal wiadomoc ok w przeciwnym wypadku kicha.
Jezeli polaczenie zostalo ustalone wysylalismy wiadomosc za pomoca metody sent. Serwer po otrzymaniu wiadomosci odsylal nam wiadomosc, ktora moglismy obsluzyc za pomoca zdarzenia onData. Kod polaczenia wyglada nastepujaco :
kontrola = new XMLSocket(); // utworzenie obiektu XML Socket
kontrola.onConnect = function(ok) // sprawdzenie czu udalo sie nawiazac polaczenie
{
trace(ok ? 'ok' : 'kicha');
};
kontrola.connect(_root.postac.adres_serwera, 10000);//polaczenie z serwerem
Metoda kontrola.connect odpowiada za polaczenie sie z serwerem na odpowiednim porcie, o adresie IP przechowywanym w zmiennej ADRES_SERWERA obiektu POSTAC (do zmiennej tej adres byl przypisany poprzez menu przez uzytkownika).Odbieranie wiadomosci bylo wykonywane przez funkcje onData uruchamiana za kazdym razem, gdy tylko serwer przeslal nam jakas wiadomosc. Ustalilismy, ze zaraz po zalogowaniu uztkownik przesle na serwer swoje dane, czyli login i klase jaka wybral :
_root.kontrola.kontrola.send(_root.postac.prefix+"logowanie;"+_root.postac.tab_nazw[0]+";"+_root.postac.tab_klas[0]+_root.postac.postfix);
Wyglada to moze skaplikowanie, ale postaramy sie jak najprosciej wytlumaczyc. Zapis _root.kontrola.kontrola odnosi sie do zmiennej KONTROLA znajdujacej sie w obiekcie o tej samek nazwie. Nastepnie wywolujemy metode SEND odpowiedzialna za przeslanie danych znajdujacych sie w nawiasach. Kolejno _root.postac.prefix jest zdefiniowana zmienna, ktora odpowiada za ustalenie naglowka wysylanego stringa. Zmienna ta ma wartosc <;#, czyli jest to przez nas zdefiniowany znacznik otwierajacy. Nastepnie przesylamy slowo logowanie; a zaraz za nim umieszczona jest nazwa (login) gracza, ktory wlasnie sie zalogowal. Po kolejnym sredniku mamy nazwe klasy, jaka dany gracz wybral, oraz znacznik konczacy nasz string, czyli # >;.
Przykladowo dla gracza o loginie Misiek i wybranej klasie CZARODZIEJ, serwer otrzyma nastepujacy string: <;#logowanie;Misiek;czarodziej# >;
Nastepnie serwer sprawdzi czy login przez nas wybrany sie juz nie powtarza. Jezeli tak, to zostanie nam odeslana wiadomosc : blad_logowania, co wymusi na programie wywolanie pierwszej klatki naszej gry, czyli menu z wyborem gracza.
Kazdy ruch naszego gracza powoduje wywolanie metody SEND zawierajacej informacje o aktualnym polozeniu gracza. Z poczatku klient Flasha wywolywal metode SEND w kazdej ramce, jednak szybko stwierdzilismy, ze jest to niepotrzebne zasmiecanie serwera danymi w przypadku gdy gracz sie nie porusza. Dlatego tez teraz metoda SEND zostaje wywolana dopiero po nacisnieciu klawisza odpowiedzialnego za ruch naszego gracza. String wysylanych danych wyglada nastepujaco : <;#dane_gracza;wspolrzedna_x; wspolrzedna_y;strzal_gracza#>;
Ponizszy send zostaje wyslany, gdy nacisniemy strzalke w prawo:
_root.kontrola.kontrola.send(_root.postac.prefix+"dane_gracza;"+pox+";"+_root.postac.gracz0_y+";"+_root.postac.wystrzal+_root.postac.postfix);
W tym zapisie zmienna POX zawiera aktualne polozenie gracza i wartosc pojedynczego kroku dla gracza, poniewaz najpierw nastepuje wyslanie danych, a nastepnie przesuniecie gracza na mapie. Nalezy tu wspomniec, ze w celu ustalenia pewnych roznic pomiedzy konkretnymi klasami postaci, zostala wprowadzona szybkosc gracza. Czyli przy wcisnietej strzalce w prawo dla czarodzieja POX wynosi _root.postac.gracz0_x+5. Zmienna gracz0_x i gracz0_y zawieraja odpowiednio przygotowane dane, ktore zawieraja polozenie gracza wzgledem mapy:
start_gracz0_x=_root.gracz0._x;//polozenia poczatkowe gracza0 start_gracz0_y=_root.gracz0._y;//polozenia poczatkowe gracza0 gracz0_x=-1*(_root.map._x-start_gracz0_x); gracz0_y=-1*(_root.map._y-start_gracz0_y);
Zmienna _root.postac.wystrzal jest ustawiana na liczby z przedzialu od 0 - 2 przy zalozeniu, ze liczby 1,2 sa ustawiane gdy gracz zwalnia klawisz spacji. Liczba 1 okresla pierwszy rodzaj ataku natomiast 2 drugi rodzaj ataku. Zaraz po wystrzale zmienna _root.postac.wystrzal od nowa jest ustawiana na 0.
Stwierdzilismy, ze poczekalnia, w ktorej gracze okreslaja swoja gotowosc nie jest dobrym pomyslem, szczegolnie dlatego, ze nie pozwala grac wiekszej grupie graczy, i uniemozliwia dolaczenie sie w trakcie gry. Dlatego zrezygnowalismy z tego pomyslu. Umozliwiajac tym samym mozliwosc dolaczania sie w trakcie gry kazdemu graczowi.
Teraz wyglada to nastepujaco. Pierwszy gracz loguje sie na serwer, wysyla swoje dane, a nastepnie jest umieszczany na planszy. W przypadku gdy drugi gracz chce sie zalogowac, wysyla swoje dane na serwer, w zamian otrzymuje dane o wszystkich graczach juz uczestniczacych w rozgrywce , ktorzy z kolei (kazdy z osobna) otrzymuje dane na temat nowego gracza.
Odbieranie danych przy wykorzystaniu zdarzenia onData wyglada następujaco :
kontrola.onData = function(d)
To, co przesyla nam serwer zostaje zapisane do zmiennej d. Oczywiscie otrzymujemy string, który nalezy jeszcze odpowiednio sformatować i odczytac. Do tego posluzyla nam wbudowana funkcja Flasha SPLIT, za pomoca ktorej mozemy podzielic string wzgledem interesujacego nas separatora. Dla nas tym separatorem byl srednik:
nazwa_gr2 = d.split(";")[0];
dx = d.split(";")[1];
dy = d.split(";")[2];
strzal = d.split(";")[3];
Dzieki temu moglismy do zmiennych przypisac już konkretne interesujace nas wartosci.
Jak juz wspomnielismy na samym poczatku serwer przysle nam dane dotyczace innych graczy. Otrzymamy string nastepujacej tresci:
nowy_gracz;nazwa_gracza;klasa_gracza
Dane te posluza nam do utworzenia kopi graczy znajdujacych sie juz w grze. W grze umiescilismy obiekty graczy, ktore posluza nam do odwzorowania innych graczy. Teraz gdy otrzymamy dane z serwera odnosnie innych graczy, Flash zajmie sie zduplikowaniem kopii gracza i umieszczeniu go w odpowiednim miejscu na planszy.
Dzieki temu widzimy kazdego gracza zalogowanego do gry. Kazdy gracz, ma swoj wlasny index dzieki czemu jest rozpoznawany przez kod programu. Dodatkowo kazda kopia gracza ma zapisany kod, za pomoca ktorego sprawdza czy dane otrzymane z serwera nie dotycza wlasnie niego. Sluzy to do odbierania wiadomosci zawierajacych dane na temat polozenia gracza.
Klient Flasha sprawdza, czy przychodzace z serwera dane mozna zmienic na dane liczbowe; jezeli tak, to zakladamy, ze przychodzace dane wstazuja na polozenie gracza, ktorego nazwa zostala przeslana w zmiennej NAZWA_GR2. Nastepnie te dane zapisywane sa w tablicy asocjacyjnej pod kluczem bedacym nazwa gracza.
Dzieki temu w kazdej chwili obiekt kazdego gracza moze dowiedziec sie o swoim aktualnym polozeniu. Gdybysmy opierali sie tylko na danych przychodzacych z serwera, musialby on caly czas przesylac aktualne polozenie kazdego gracza nawet wtedy, gdyby wszyscy stali w miejscu. Tak zostal rozwiazany problem poruszania sie graczy po planszy. Dzieki takiemu rozwiazaniu widzimy wszystkich uzytkownikow grajacych w nasz gre.
W pierwszej wersji gry obiekt KONTROLA zajmowal sie przegladaniem tablicy wszystkich graczy i ustawianiu ich polozenia na mapie jednak bylo to rozwiazania bardzo pracochlonne i niepotrzebnie opozniajace dzialanie gry. Teraz obiekt KONTROLA zajmuje sie jedynie odbieraniem danych z serwera. Kazdy klon gracza sam rownolegle sprawdza czy dane przychodzace na serwer naleza do niego. Jezeli tak wywoluje to odpowiednie dzialanie na danym obiekcie.
W grze mamy mozliwosc wykonania ataku kazdym z graczy. I tak jezeli wybierzemy lucznika, to mozemy strzelac za pomoca strzal.
Na planszy znajduja sie zawsze para obiektow jeden jest odpowiedzialny za akcje gracza glownego (tego kierowanego przez nas), natomiast drugi z przyrostkiem _GOSC posluzyl nam do pokazania akcji wszystkich innych graczy korzystajacych z tej samej klasy postaci.
Jezeli chodzi o gracza glownego, to biorac pod uwage fakt, ze jest to postac statyczna, to mozemy zalozyc, ze wystrzelona przez niego strzala zawsze bedzie startowac z jednego punktu. Natomiast kierunek strzalu bedzie zwiazany z kierunkiem gracza. Aby uniemozliwic ciagle strzelanie zastosowalismy zapadnie za pomoca ktorej obiekt strzaly pojawia sie dopiero po zwolnieniu klawisza spacji. Kod przedstawiamy ponizej:
if(Key.isDown(Key.SPACE))
flag1=1;
if (!Key.isDown(Key.SPACE) && flag1==1)
{
flag2=1;
flag1=0;
}
else
flag2=0;
Strzal zachodzi gdy wartosc zmiennej FLAG2 jest ustawiona na 1. Sam obiekt strzaly posiada swoj wlasny kod odpowiedzialny za jej tor lotu. Duzym problemem byl fakt, ze jezeli gracz sie poruszal, to lecaca strzala poruszala sie razem z nim (jak juz wczesniej wspominalismy w rzeczywistosci to mapa sie przesuwala a nie gracz).
Aby zniwelowac ten problem musielismy odpowiednio zmodyfikowac kod strzaly. Ponizej przedstawimy jak wyglada kod strzaly wplywajacy na jej lot, gdy zostanie ona wystrzelona w prawa strone:
if(kier == 'p')
{
if(licznik <; war_stopu)
{
wx +=szybkosc_strzalu;
_x =_root.map._x-baza_x+gracz_x+wx;
_y =_root.map._y-baza_y+gracz_y;
}
else
{
unloadMovie(this);
}
}
Nalezy tu wspomniec, ze mamy pelna kontrole nad strzala, tzn. mozemy wplywac na jej szybkosc oraz zasieg. Oczywiscie mowa tu o stronie programowej, dzieki temu zastosowaniu mozemy w przyszlosci rozbudowac gre ustawiajac jakies komponenty sluzace do przyspieszania strzal lub ich zwalniania cos w rodzaju powerup-ow.
Wracajac do kodu najpierw jest sprawdzany kierunek stzralu, nastepnie w zmiennej baza_x przechowujemy polozenie mapy w chwili wystrzalu. Jest to wartosc niezmienna. Kolejno obliczamy polozenie naszej strzaly, az do chwili osiagniecia warunku stopu, czyli zasiegu strzalu.
Czyli od aktualnego polozenia mapy odejmujemy jej polozenie w chwili strzalu i dodajemy wspolrzedne gracza (te rowniez w przypadku gracza glownego sa ustawione na stale). Aby strzala sie poruszala dodajemy jeszcze wartosc WX bedaca wektorem przesuniecia naszej strzaly. Jezeli warunek stopu jest spelniony kasujemy nasza strzale. Jezeli nasza strzala trafi w cel (tu innego gracza), nastepuje wyslanie odpowiedniej wiadomosci do serwera :
for(var x=1;x<;_root.postac.tab_nazw.length;x++)
{
if(this.hitTest(_root['gracz'+x]))
{
nazwa_gracza_trafionego = _root['gracz'+x].nazwa_gracza;
_root.kontrola.kontrola.send(_root.postac.prefix+'trafienie;'+nazwa_gracza_trafionego+';'+_root.gracz0.czar+_root.postac.postfix);
unloadMovie(this);
}
}
Lecaca strzala w kazdej klatce naszej gry sprawdza, czy nie trafila ktoregos z graczy. Jezeli tak wysyla do serwera string:
<;#trafienie;nazwa_gracz_trafionego;rodzaj_ataku#>;
Dzieki temu serwer moze wykonac stosowne obliczenia dotyczace zadanych obrazen. Odrebna postac jaka jest wojownik, ktory po nacisnieciu klawisza spacji wykonuje ruch toporem, a nie jego rzut.
Jezeli chodzi o innych graczy, ktorych widzimy na planszy, to jezeli tylko otrzymamy od serwera wiadomosc o wykonaniu jakiejs akcji przez gracza (tu strzalu). Odpowiedni obiekt gracza (zaluzmy, ze rowniez jest to lucznik) skopiuje obiekt strzaly goscia. Strzala goscia ma troche inny kod niz strzala glownego gracza, glownie z tego wzgledu, ze jej polozenie startowe moze sie zmieniac wraz ze zmiana polozenia gracza.
Natomiast reszta jest praktycznie niezmieniona. Dzieki temu widzimy latajace strzaly pochodzace od wszystkich graczy. Dokladnie na takiej samej zasadzie zrobiony jest obiekt sciany ognia, z ta mala roznica, ze gdy sciana ognia trafi na innego gracza, to nie zostaje od razu usuwana z planszy zadajac ciagle obrazenia, do czasu az funkcja hitTest przestanie zwracac wartosc true.
W naszej grze kazdy z graczy posiada po dwa rozne czary. Dodatkowo w czarach wyzszego rzedu uzylismy pewnego opoznienia czasowego, tak aby nie mozna bylo korzystac z nich w sposob ciagly. Opoznienie uzyskalismy za pomoca licznika inkrementowanego w kazdej klatce i zerowanego podczas strzalu. Nie mozna wykonac strzalu dopuki licznik nie osiagnie okreslonej wartosci.
W prawym dolnym rogu mamy menu okreslajace stan naszej postaci. Czyli poziom zycia, ktory jest ustawiany przez serwer, za pomoca funkcji :
function zostalem_trafiony(ile,max_life)
{
var zycie =ile*164/max_life;
var pasek_zycia = 164-zycie;
_root.pasek_stanu.life._y=_root.pasek_stanu.life.stan_pocz+pasek_zycia;
if(ile==0)//smierc bohatera
{
_root.gracz0.blokada_postaci=1;
_root.map._x=0;//przeniesienie do punktu poczatkowego
_root.map._y=0;//przeniesienie do punktu poczatkowego
_root.smierc.gotoAndPlay('SMIERC');
_root.gracz0.blokada_postaci=0;
trace('smierc');
}
}
Liczba 164 jest wysokoscia paska zycia, jego aktualny poziom jest obliczany za pomoca proporcji. Jezeli ilosc zycia osiagnie poziom zero, to gracz zostaje przeniesiony na miejsce startowe i zostaje wyswietlony napis.
Obok paska zycia mamy pasek charakteryzujacy ile jeszcze pozostalo nam atakow. Liczba atakow zmiejsza sie az do zera po kazdym ataku. Pasek mozna uzupelnic wchodzac na kolczan ze strzalami.
Po lewej stronie znajduje sie rozwijalne menu, w ktorym mozemy sledzic nasze aktualne charakterystyki. Sa one przesylane z serwera zaraz po zalogowaniu i za kazdym razem, gdy uzyskamy wyzszy poziom doswiadczenia.
Miejsce startowe jest polem neutralnym nie mozna na mim atakowac, wszystkie strzaly, ktore beda probowaly byc wystrzelone w tym polu zostana usuniete.