Tutoriale 39dll. Tłumaczenie.
Jak używać protokołów UDP i TCP jednocześnie w grach online, które korzystają z 39dll.dll stworzonego przez 39ster? Używamy przykładu PONG.
[ROZMIAR=14px]PO PIERWSZE : ZALETY UDP[/ROZMIAR]
Największą zaletą jest jego szybkość i niewielka przepustowość używana podczas wykorzystywania tegoż protokołu. UDP jest znacznie szybszy niż TCP i powinien być wykorzystywany w grach, w których główne pakiety przysyłane są przez cały czas. UDP wykorzystuje łącze mniej brutalnie, co oznacza, że jest bardziej skuteczniejszy i szybszy. Działa on na prostej zasadzie :
Wiadomość----->Odpowiednie IP i Port
Wiadomość jest wysyłana bezpośrednio do odpowiedniego IP i portu i już;
[ROZMIAR=14px]PO DRUGIE : WADY UDP[/ROZMIAR]
Jedną z, niewielu oczywiście, wad UDP jest to, że jest nie za bardzo solidny. Nie ma w nim żadnej gwarancji, że pakiety dotrą. Dlatego właśnie łączymy TCP i UDP, żeby wykorzystywać wolniejszy, ale pewny TCP do przesyłania ważnych pakietów poprzez jego socket. Mniej ważne pakiety powinny być przesyłane przez UDP, bo nawet jeśli nie dotrą, to bezpośrednio po nich wysyłane są kolejne pakiety.
Kolejną wadą UDP jest to, że Firewall ma pewne problemy z UDP. Protokół ten potrzebuje specjalnej konfiguracji Firewalla tylko jeśli hostujesz grę za pomocą TCP. Gdy korzystasz z UDP to nawet klient potrzebuje odpowiedniej konfiguracji Firewalla.
To teraz przejdźmy do samego kodu.
[ROZMIAR=14px]Inicjacja dlla :[/ROZMIAR]
Aby dll funkcjonował należy podczepić jego skrypty do gry :
Otwieramy GMa > Z menu wybieramy Scripts > Import Scripts > Dllscripts.gml z folderu, gdzie znajduje się 39dll z wszystkimi plikami.
Po imporcie skryptów inicjujemy dla za pomocą kodu:
kod
dllinit(0, true, false);
Jeśli pierwszy argument jest liczbą zostaje załadowana domyślna nazwa dlla(39dll.dll). Jeśli dll ma inną nazwę(np. socket.dll) należy ją podać właśnie w tym argumencie.
Drugi argument odpowiada za używanie winsock, w tym przypadku jest to konieczne.
Trzeci argument powinien być ustawiony na true tylko jeżeli chcemy używać funkcji związanymi z plikami. W przykładnie dołączonym do 39dll jest ustawiony na false, gdyż nie potrzebujemy tych funkcji.
[ROZMIAR=14px]STAWIANIE SERWERA:[/ROZMIAR]
Aby stworzyć grę multiplayer, jedna osoba musi postawić serwer, czyli hostować, a druga osoba musi się pod ten serwer podłączyć.
Jeżeli chcesz dać możliwość postawienia serwera za pomocą tego dlla musisz stworzyć obiekt, który będzie obsługiwał ustawienia serwera i akceptował nowe połączenia. Przejdźmy do praktyki
Tworzymy dwa obiekty, które będą przyciskami, nadajemy im sprite'a. Jeden przycisk to HOST, drugi CONNECT. Kiedy gracz wybierze HOST jego kod:kod
global.master = true; // hostowanie
powinien tak wyglądać, jak wyżej. Powinniśmy teraz stworzyć nowy room, do którego będzie przenosić, gdy klikniemyHOST. Nazwijmy go rmWaiting. Teraz stwórzmy nowy obiekt: objWait. W CREATE tego obiektu umieszczamy kod:
kod
listen = tcplisten(14804, 2, true); //nasłuchiwanie
if(listen, <= 0) //jeśli nie udaje się połączyć
{
show_message(Failed to listen on port 14804); //pokaż wiadomość
game_end(); //zakończ grę
}
Kod ten tworzy nowy soket : nasłuchiwanie. Nasłuchuje on przychodzące połączenia na porcie 14804. Numer portu może być taki, jaki sobie wybierzesz, ale my używamy tutaj 14804. Drugi argument oznacza maksymalną możliwą liczbę połączeń na liście. PAMIĘTAJ : TO NIE JEST MAKSYMALNA MOŻLIWA ILOŚĆ GRACZY W GRZE. Jeśli ktoś stara się dołączyć do gry, to zostaje zapisany na listę, wtedy serwer może zaakceptować jego połączenie. Ostatni argument jest na true, bo nie chcemy zamrażać nasłuchiwania, gdy użyjemy skryptu tcpaccept();. Socket ten zwróci nam numer identyfikacyjny, który będzie większy niż 0, gdy połączenie osiągnie sukces. Jeśli nie uda nam się połączyć zwracamy numer niższy bądź równy 0.
Kolejna linijka kodu sprawdza, czy nasłuchiwanie jest pozytywne, czy nie. Jeśli nie to wtedy wyświetla wiadomość, iż nie udało nam się połączyć z serwerem.
[ROZMIAR=14px]AKCEPTOWANIE POŁĄCZENIA:[/ROZMIAR]
Aby zaakceptować nowe połączenia musimy w naszym obiekcie ObjWait, w evencie STEP wpisać kod :
kod
client = tcpaccept(listen, true); //akceptowanie
if(client <= 0) exit;
global.udpsock = udpconnect(14805, true);
global.otherplayer = client;
global.otherip = lastip();
global.otherudpport = 14803;
room_goto(rmGame);Pierwsza linijka sprawdza listę oczekujących połączeń, aby zobaczyć czy ktoś nie próbuje dołączyć się do serwera na nasłuchiwanym porcie. Jeśli nikt nie chce się dołączyć to polecenia zwraca liczbę mniejszą od 1. Jeśli natomiast ktoś dołączy to polecenie tworzy i zwraca ID nowego socketu. Od tego momentu socjet będzie służył to wysyłania i przyjmowania informacji od gracza, który dołączył do gry. Kolejny argument w tcpaccept oznacza to, że nowy socket nie będzie blokowany, w znaczeniu, iż jeśli próbujesz odebrać jakieś wiadomości, to w przypadku, gdy nie będzie żadnych wiadomości do odebrania gra się nie zamrozi. Druga linijka kodu sprawdza, czy tcpaccept nie napotkał błędów. Jeśli napotkał, to przerywa działanie skryptu. Każda następna linijka kodu, będzie wykonana tylko wtedy, gdy tcpaccept nie napotkał błędu. Trzecia linijka kodu tworzy nowy socket, ale tym razem w oparciu o protokół UDP. Będzie on służył do komunikacji dzięki temu protokołowi.
Następna linijka tworzy nową zmienną globalną global.otherplayer dla socketu, który został zwrócony poprzez tcpaccept(). Następna linijka zwraca natomiast IP gracza, kótry jako ostatni zaakceptował połączenie i zapisuje ten adres to zmiennej globalnej global.otherip. Wykorzystywane to będzie, podczas przesyłania wiadomości poprzez UDP do innych graczy. Kolejna linijka kody zapisuje port, który wykorzystuje komputer podłączający się do serwera, czyli nasz port UDP w zmiennej globalnej global.otherudpport. Będzie to potrzebna, podczas wysyłania wiadomości do innych graczy. Ostatnia linijka przenosi nas do pokoju, z naszą prawdziwą grą, w który znajdą się gracze :P.
[ROZMIAR=14px]PODŁĄCZANIE SIĘ DO SERWERA.[/ROZMIAR]
Aby dołączyć do gry multiplayer musimy podłączyć się do serwera. W roomie, gdzie znajdują się dwa przyciski (HOST i CONNECT) jeden przycisk ma już swój kod, ale co z drugim? No i tą kwestią się teraz zajmiemy. Wybieramy nasz obiekt-przycisk. W kodzie naciśnięcia LPM dajemy kod:
kod
global.master = false;
server = tcpConnect("127.0.0.1", 14804, true);
if(server <= 0)
{
show_message( "Unable to connect to server" )
game_end();
exit;
}
global.otherplayer = server;
global.udp = udpconnect(14803, true);
global.otherip = tcpip(server);
global.otherudpport = 14805;
room_goto(rmGame);
Pierwsza linijka ustawia naszą zmienną globalną master na false, bo nie jesteśmy graczem hostującym, czyli po prostu nie jesteśmy serwerem. Jesteśmy zwykłym graczem, łączącym się z serwerem. Druga linijka tworzy aktualne połączenie z serwerem. Pierwszy argument tcpConnect to adres IP, do którego chcemy się podłączyć. Jeśli po prostu testujesz lokalnie, to pozostaw localhost, czyli 127.0.0.1. Drugi argument to port, do którego się podłączamy. Trzeci argument dotyczy używania trybu blokującego, czy też nie. Ustawiamy go na true, co oznacza tryb nieblokujący. Używanie tego trybu oznacza, że gdy wysyłamy bądź odbieramy wiadomość to gra nie zamraża się, aż wykona operację. Jeśli to polecenie wykona się pozytywnie i podłączenie zostanie zaakceptowane przez serwer to zmienna server powinna zawierać ID socketu. Jeśli napotka błędy zwróci liczbę mniejszą niż 1.
Następna linijka sprawdza, czy występują błędy. Błędy mogą występować z różnych powodów, np. serwer odrzuca nasze połączenie czy też po prostu nie istniej. Jeśli wystąpi jakikolwiek błąd gra zostanie zamknięta, pokaże się wiadomość i zakończy się wykonywanie poleceń.
Jeśli nie napotkamy żadnych błędów, to zmienna globalna otherplayer przyjmie wartość ID socketu, czyli wartość zmiennej server. Następnie zostanie otworzony socket UDP na porcie 14804 i ustawiony na tryb nieblokujący. Następna linijka odzyska adres IP serwera i zapisze go do zmiennej global.otherip, która będzie używana podczas wysyłania wiadomości poprzez UDP. Następna linijka przechowuje numer portu, którego hostujący używa to jego socketu UDP i zapisuje ten numer w zmiennej global.otherudpport. Następnie gra rozpoczyna się poprzez przejście do roomu z grą.
[ROZMIAR=14px]WYSYŁANIE I ODBIERANIE WIADOMOŚCI:[/ROZMIAR]
Aby nasza gra działała poprawnie musimy znać pozycję innego gracza oraz innych obiektów na mapie(np. w przykładzie PONG, musimy znać x i y piłeczki oraz y paletki przeciwnika).
WYSYŁANIE
Posłużmy się przykładem PONGa stworzonym przez autora 39dll.dll.
Paletka sterowana przez Ciebie musi wysyłać swoją pozycję do innego gracza, aby na jego ekranie pojawiła się w odpowiednim miejscu(w tym samym, co u Ciebie). Aby to zrobić umieszczamy ten kod w evencie naciśnięcia strzałki w górę i w dół:
kodclearbuffer();
writebyte(0);
writeshort(y);
sendmessage(global.udpsock, global.otherip, global.otherudpport);Pierwsza linijka czyści wewnętrzny bufor z wszystkich informacji znajdujących się tam aktualnie. Druga linijka wysyła jeden bajt, który zastępuje ID wiadomości. W naszej grze wiadomość o ID=0 będzie wskazywać na wiadomość z pozycją y paletki. Następna linijka wysyła aktualną pozycję Y do buforu. Użyliśmy tu konstrukcji short, ponieważ ta konstrukcja może oznaczać każdy numer pomiędzy -32000 a +32000. Konstrukcja ta wykorzystuje dwa bajty. Jeżeli użyjemy tylko jednego bajta, aby przesłać pozycję y, a y będzie większy niż 255 to się skończy, a tego oczywiście nie chcemy. Ostatnia linijka wyśle z naszego buforu wewnętrznego do przeciwnika za pomocą socketu UDP wszystkie informacje, (czyli pozycję y). Drugi argument sendmessage to IP osoby, do której wysyłamy wiadomość, a trzeci argument to port, do którego wysyłamy wiadomość. W tym przypadku wiadomość zawiera ID wiadomości i 2 bajty z pozycją y.
A teraz musimy wysłać pozycję x i y piłeczki do innego gracza, oczywiście, jeśli jesteśmy graczem-serwerem. Aby to zrobić umieszczamy dany kod w vencie step piłeczki:kod
if(!global.master)exit;
clearbuffer();
writebyte(1);
writeshort(x);
writeshort(y);
sendmessage(global.udpsock, global.otherip, global.otherudpport);
Pierwsza linijka sprawdza czy jesteśmy graczem-serwerem, jeśli NIE jesteśmy to kończy skrypt i nie wykonuje kodu poniżej.
Jednakże, kiedy serwerem jesteśmy to po pierwsze czyścimy bufor wewnętrzny.
Teraz przypisujemy wiadomości ID=1, czyli wiadomość oznaczającą pozycję piłeczki. Teraz wysyłamy dwie konstrukcję short z odpowiednio pozycją x i y. Następnie wysyłamy tą wiadomość poprzez socket UDP, do gracza o danym IP i porcie.
[ROZMIAR=14px]WYSYŁANIE WIADOMOŚCI W POSTACI CHATU:[/ROZMIAR]
Oto kod, którego używamy do wysyłania wiadomości zawierających tekst bądź liczby niebędące zmiennymi:kod
clearbuffer();
writebyte(2); // ID wiadomości chatowej
writestring('Hello other player');
sendmessage(global.otherplayer);Pierwsza linijka czyści bufor, druga to ID wiadomości. Trzecia wysyła wiadomość do buforu, czwarta wysyła wiadomość do przeciwnika, ale z wykorzystaniem TCP a nie UDP, gdyż chcemy mieć pewność, że wiadomość dotrze.
[ROZMIAR=14px]ODBIERANIE WIADOMOŚCI.[/ROZMIAR]
Posłużmy się znów przykładem PONGA. W paletce kontrolowanej przez przeciwnika, w vencie step umieszczamy kod:
kodvar size;
while(true)
{
size = receivemessage(global.udpsock); //try receive a message on the udp socket
if(size <= 0) size = receivemessage(global.otherplayer); //if no udp message try receive a tcp message
if(size < 0) break;
if(size == 0)
{
show_message("The other player left the game")
game_end();
}
messageid = readbyte();
switch(messageid)
{
case 0:
y = readshort();
break;
case 1:
objBall.x = readshort();
objBall.y = readshort();
break;
case 2:
chatmessage = readstring();
show_message(chatmessage);
break;
}
}
Na początku tworzymy nieskończoną pętle używając while(true).Pierwsza linijka pętli odbiera wszystkie widomości od innego gracza przesłaną dzięki socketowi UDP i ustawia zmienną size na liczbę bajtów odebranych. Druga linijka sprawdza czy aby na pewno nie odebrano żadnych wiadomości. Jeżeli żadna wiadomość nie została odebrana, to wtedy próbujemy odebrać wiadomości wysłane przez socket TCP. Jeśli żadna wiadomość nie została odebrana przez TCP to kończymy pętle. Następna linijka sprawdza czy przeciwnik czasem nie rozłączył się, czyli nie odszedł z gry. Jeżeli zmienna size wynosi 0 to oznacza, że przeciwnik opuścił grę. Jeżeli już to zrobił, to kończymy grę. Jeśli zaś odebraliśmy wiadomość to wszystkie informację znajdują się w wewnętrznym buforze. Teraz możemy użyć skryptów bufora by zwrócić wszystkie informacje z wiadomości. Pierwsza część zwraca ID wiadomości(messageid = readbyte()). Po tym używamy deklaracji swich() aby sprawdzić, ile wynosi ID wiadomości. Jeśli wynosi 0 to oznacza to, że wiadomość jest pozycją y dla innych graczy. Używamy teraz prostego readshort() aby zwrócić pozycję y. Pamiętaj, że wysyłając wiadomość konstrukcją short tą samą konstrukcją musimy ją odebrać!
Jeśli ID wiadomości wynosi 1, czyli wiadomość oznaczającą pozycję piłeczki, to używamy ponownie readshort(), aby zwrócić pozycję x piłeczki, a następnie wykonujemy to samo z pozycją y.
Jeżeli zaś ID wynosi 2, co oznacza wiadomość czatową, to kopiujemy tą wiadomość do zmiennej chatmessage używając readstring() i wyświetlamy ją.
[ROZMIAR=14px]CZYSZCZENIE DLLA[/ROZMIAR]
Jeśli nie chcesz żadnych nieprzyjemnych błędów, podczas zamykania gry, musisz oczyścić dlla z pamięci. Aby to zrobić tworzymy nowy obiekt i umieszczamy go w każdym roomie. W vencie Game End umieszczamy tylko jedną linijkę kodu :
koddllfree();Oczyści to całego WINSOCKa i pamięć używaną przez bufor wewnętrzny.
[ROZMIAR=14px]ZAMYKANIE SOCKETÓW:[/ROZMIAR]
Kiedy zakończysz pracę z dllem powinieneś zamknąć wszystkie sockety otwarte podczas gry. Aby to zrobić użyj:
closesocket(socketid);
Na wszystkich socketach, których użyliśmy. Nie zapomnij zamknąć także socketu nasłuchującego.
To tyle na ten nudny temat. Dzięki za przeczytanie Arta. :D