Unity3d QuickTip – czyli szybkie porady, rozwiązania częstych problemów i sztuczki w Unity3d!

Dzisiejszy odcinek: Multiplayer w Unity3d z wykorzystaniem UNet. Część 1

Uwaga! Jest to poradnik typu QuickTip. Zatem skupia się on na osiągnięciu założonego celu. Zatem zakładamy że użytkownik zna na tyle program Unity3d, aby samodzielnie wykonać najprostsze czynności, jak np. dodanie modelu kostki do sceny czy dodanie modelowi jakiegoś komponentu. Jeżeli brakuje Ci tej podstawowej wiedzy, zapraszam do tutoriala:
Unity Tutorial – Podstawy

Teoria

Czym jest multiplayer chyba nie muszę tłumaczyć. W tym przykładzie będziemy korzystać z wprowadzonego w nowym Unity systemu UNet, który pozwala uruchomić grę w trybie multiplayer w bardzo szybki i prosty sposób.

Z racji, że omawiamy tutaj bardziej zaawansowane zagadnienia, nie będę opisywał rzeczy typu jak dodać Cube, albo jak importować paczkę assetów. Zakładam, że użytkownik podejmujący się próby napisania trybu multiplayer potrafi takie rzeczy.

Warto też wiedzieć jak działa mniej więcej sieć komputerowa i czym jest architektura klient – serwer (client – server). Nie chodzi tu o dokładne rozeznanie w architekurze TCP-IP, ale o podstawową wiedzę czym jest owy klient i czym jest serwer.

Przygotowanie

Aby zacząć przygodę z multiplayerem, najpierw musimy wykonać trochę kodu i czynności pod single player. Streszczając, potrzebujemy jakiejś podstawowej mechaniki. Od razu muszę tutaj zmartwić osoby, które spodziewały się, że po tym poradniku będą miały gotowego Counter-Strike’a. Pokażę tutaj podstawowe zasady budowania gry Multiplayer z wykorzystaniem UNet. Przyłączenie tego mechanizmu do waszej gry, pozostawiam wam.

W sumie wszystko co nam będzie na razie potrzebne, to prosty Cube, do którego dopiszemy sobie skrypt. U mnie będzie to Moving.cs. Skrypt dopisujemy do utworzonego Cuba (przeciągając skrypt na niego). Wchodzimy w edycję skryptu i wprowadzamy tam coś takiego:

No to wyjaśnimy sobie na szybko bardziej zawiłe elementy i rzeczy na które trzeba zwrócić uwagę.

Tutaj mamy prostą strukturę, do przechowywania pozycji naszej kostki w świecie gry. Funkcję Awake i wywoływaną w niej funkcję pomijam, bo to jedynie inicjacja początkowych wartości.

W funkcji Update sprawdzamy sobie wszystkie klawisze strzałek. Jeśli któryś jest wciśnięty, to pod nasz obecny stan kostki (jej pozycję) podstawiamy sobie wynik funkcji Move. Na końcu każdej klatki synchronizujemy stany.

Synchronizacja to nic innego jak przypisanie do naszej aktualnej pozycji, wartości z zapisanego stanu. Na koniec została funkcja Move. Jej nie rozpisuję, bo jedyne co tam robimy, to określamy który klawisz został wciśnięty i na tej podstawie modyfikujemy naszą zmienną state, zwracając aktualny stan kostki. Jak wiemy z wcześniejszego kawałku kodu, następnie funkcja synchronizacji przypisze nowy stan, do faktycznej pozycji kostki.

Jeśli teraz uruchomisz sobie grę, to za pomocą strzałek możesz normalnie ruszać kostką.

Magia UNet i Multiplayer

Ale przyszliśmy tu pracować nad Multiplayerem, prawda? Środowisko UNet jest bardzo przyjemne i proste w obsłudze, ponieważ opiera się na gotowych komponentach, które wystarczy odpowiednio przypisać i skonfigurować. Do tego wystarczy kilka zmian w kodzie i będziemy się cieszyć naszą grą multiplayer.

Pierwsze co musimy sobie zrobić, to umożliwić graczom na połączenie z serwerem. Posłużą nam do tego specjalne komponenty. Tworzymy sobie pusty GameObject (Ctrl + Shift + N). Nazywamy go sobie dowolnie (u mnie jest to NetworkManager). Do obiektu dodajemy dwa komponenty: NetworkManager i NetworkManagerHUD. Oba są dostępne z poziomu: [Component -> Network].

Na szybko wyjaśnimy czym są oba komponenty. Pierwszy (NetworkManager) to główny komponent zarządzający połączeniem. Dzięki niemu możemy przesyłać komunikaty między klientem i serwerem, tworzyć serwery etc.

NetworkManagerHUD pozwala nam wyświetlić proste menu to tworzenia gier multiplayer.

Teraz czas na szok. Tyle wystarcza, żeby instancja gry mogła utworzyć serwer (zostać hostem), albo inna mogła się podłączyć do tego serwera jako klient. Ale samo połączenie to nie wszystko, bo nasz obiekt gracza nie jest przystosowany do bycia obiektem multiplayer.

Przystosowanie gracza do Multiplayer

Pierwsze co należy zmienić do dodać do naszego obiektu gracza (kostki) tożsamość sieciową – brzmi dziwnie, ale chodzi o to, żeby poinformować sieciowe API, że należy się zaopiekować wartościami sieciowymi tego obiektu. Mówiąc bardziej obrazowo. Jeśli mamy MMO to dla wszystkich graczy budynki, ziemia itp. są w tym samym miejscu i się nie ruszają. Za to przeciwnicy i sami gracze przemieszczają się. Dlatego budynki nie muszą mieć tożasmości sieciowej, bo wszędzie i zawsze będą w tym samym miejscu, więc można odciążyć łącze i wyświetlić je graczowi lokalnie. Za to przeciwnicy i inni gracze muszą mieć tożsamość, aby serwer mógł im synchronizować pozycję, dzięki czemu u każdego gracza dany przeciwnik jest w tym samym miejscu i robi to samo.

To jak to osiągnąć? Również dość prosto – przynajmniej na początku. Wracamy do naszej kostki (zaznaczamy ją) i dodajemy do niej komponent NetworkIdentity. (Znów menu: [Component -> Network -> NetworkIdentity]). Serio, tyle wystarczy.

Teraz musimy zmienić nasz obiekt gracza na prefab (wystarczy przeciągnąć go ze sceny do panelu Project). Obiekt znajdujący się na scenie możemy już usunąć.

Wybieramy sobie ponownie nasz NetworkManger i znajdujemy w nim opcję PlayerPrefab, gdzie przeciągamy nasz przed chwilą utworzony prefab.

Dodanie Prefabrykatu do NetworkManagera
Dodanie Prefabrykatu do NetworkManagera w UNet

Jesteśmy bardzo blisko, ale to jeszcze nie zadziała, bez lekkiej modyfikacji kodu.

UNet NetworkBehaviour

Wracamy do edycji naszego skryptu. Pierwsza zmiana to podmiana legendarnego MonoBehaviour na NetworkBehaviour:

NetworkBehaviour jest w sumie tym co MonoBahaviour ale wzbogacone o funkcje umożliwiające wymianę danych między klientem i serwerem. Jednak sama podmianka nie załatwi wszystkiego, bo na samej górze musimy sobie dodać coś takiego:

Bez bibliotek sieciowych, zwyczajnie sieciowej gry nie zrobimy. Kolejna zmiana to dodanie słowa kluczowego [SyncVar] do naszego stanu kostki:

Co daje SyncVar? Zapewnia nam to, że jeśli wartość zmiennej po [SyncVar] zostanie zmieniona na serwerze, to serwer rozsyła informację z nową wartością do wszystkich podłączonych klientów. Należy tutaj pilnować, żeby wszystkie zmiany zmiennych z ustawionym [SyncVar] dokonywane były z poziomu hosta.

Jeżeli teraz przetestowalibyśmy grę, okazałoby się, że jeśli naciśniemy strzałkę, to wszystkie podpięte kostki wykonają ruch. Trochę bez sensu, ale żeby to ogarnąć wystarczy prosta zmiana:

isLocalPlayer to zmienna mówiąca o tym, czy dany obiekt gracza (czyli prefab, który dodaliśmy wcześniej do NetworkManagera) to nasz obiekt, czy nie. Innymi słowy, ten if sprawia, że sprawdzamy stan naciśnięcia klawiszy i dokonujemy ewentualnego ruchu, tylko wtedy kiedy mamy do czynienia z naszym własnym obiektem gracza.

No dobra, ale powiedzieliśmy sobie, że zmienne z [SyncVar] powinny być aktualizowane tylko przez serwer, co aktualnie nie ma miejsca. Więc musimy coś z tym zrobić. UNet znów dostarcza do tego wygodne narzędzie jakim są komendy (Commands). Jest to nic innego jak funkcja na serwerze, którą można wywołać z poziomu klienta.

Zasada zmiany funkcji w komendę jest bardzo prosta. Znów dodajemy sobie słowo kluczowe [Command] przed nazwą funkcji, a do jej nazwy dopisujemy przedrostem Cmd. Żeby nie paskudzić dodatkowo kodu, tworzymy sobie dodatkową funkcję:

Jak widać, nie ma tutaj jakichś cudów. Robi ona dokładnie to, co wcześniej robiliśmy w funkcji Update przyjmując tylko naciśnięty klawisz. Robimy jeszcze jedną zmianę:

Więc co się właściwie stało? Technicznie wygląda jak byśmy nic nie zrobili. Ale różnica jest znaczna. Teraz, gdy gracz naciśnie strzałkę, to zamiast modyfikować zmienną stanu, wywołujemy sobie funkcję CmdMoveOnServer podając jej, jaki klawisz został naciśnięty. Zaś sama funkcja znajduje się już na serwerze i to ona wykonuje obliczenia jak zmieniła się pozycja postaci. Osiągnęliśmy to co chcieliśmy, czyli teraz tylko serwer modyfikuję zmienną state, która ma [SyncVal].

Warto tutaj zwrócić uwagę na jeszcze jeden fakt. Nasze wywołanie ruchu wykonujemy dalej wewnątrz ifa z isLocalPlayer, dzięki czemu zmianę pozycji wykonujemy tylko dla obiektów, które należą do gracza. Dzięki temu uniemożliwiamy graczowi przejęcie kontroli nad obiektami innego gracza, a dodatkowo na serwerze mamy pewność kto wywołał daną akcję, co znacząco zwiększa bezpieczeństwo naszej gry sieciowej.

Został nam ostatni problem, a mianowicie funkcja InitState, która inicjuje początkowe wartości zmiennej state. Czemu to jest problem? Ponownie zmienna state posiadająca atrybut [SyncVal] może być modyfikowana tylko przez serwer. Tutaj znów nas ratuje UNet, ze swoim kolejnym atrybutem [Server]

Dzięki temu, funkcja wywoła się na serwerze. W przypadku, gdy funkcja zostaje wywołana na kliencie, wywołanie zostaje zignorowane.

Na razie tyle.

Jak testować?

Zostało zasadnicze pytanie. Jak to teraz przetestować? Jest w sumie jeden sposób. Należy zrobić sobie build gry [File -> Build Settings]. Jeśli w okienku Scenes In Build nie ma ani jednej sceny, można skorzystać z przycisku Add Open Scene. (Jeśli do tej pory nie zapisaliśmy sceny teraz będzie trzeba to zrobić). Potem klikamy klawisz Build i czekamy. Gdy mamy gotowy build odpalamy sobie grę, wybieramy tam: LAN Host. Następnie uruchamiamy grę z edytora Unity, klikając Play i z menu, które się pojawi wybieramy LAN Client.

Efekt końcowy (GIF)
Efekt końcowy (GIF)

Teraz pewnie rzuca się w oczy jedna rzecz. Mamy straszne lagi. Wynika to z opóźnień. Zanim wyślemy komunikat o kliknięciu klawisza do serwera, zanim on obliczy nową wartość i ponownie ją zwróci mija niezerowy czas. Przez to pojawiają się lagi. Są na to metody i jedną z nich omówimy sobie kolejnym razem.

  • Bartek

    Świetny Poradnik, ale tutaj Gracz jest hostem. Jak zrobić osobno Hosta(server bez gracza) i osobno klientow ?

    • Takie tematy będę rozwijał w późniejszych częściach ;)

    • Marcin Milczarek

      Chyba wszyscy czekają na coś takiego. Pozdrawiam, świetny tutorial :)

    • dziękuje za tutorial

  • Dziecko We Mgle

    Obecnie multi niby jest ale go nie ma :/ bo na jednym kompie sam ze sobą to i bez tego pograć mogę. Czekam na kolejną część, bo gdy odpaliłem na 2 komputerach jednocześnie to nie było oczekiwanego efektu :/

    • Przy uruchamianiu wybieramy sieć LAN i port 7777. Jeżeli komputery nie są w tej samej sieci, albo łączą się na różnych portach, to coś faktycznie może nie działać.

      Dodatkowo serwer jednej z maszyn to Localhost, czyli ona sama. Żeby osiągnąć efekt pomiędzy dwoma maszynami musisz znać IP drugiej i to je podać jako adres łączenia się. Localhost to nic innego jak adres 127.0.0.1, czyli odwołanie do samego siebie. Więc jeśli nie modyfikowałeś w żaden sposób parametrów to Twój jeden komputer stworzył hosta u siebie, a drugi szukał hosta… też u siebie. Przy uruchamianiu drugiej instancji gry, masz pole żeby podać adres (wpisane obecnie tam jest: „localhost”).

      W przypadku łączenia się pomiędzy dwoma kompami, mogą też wystąpić problemy sieciowe, typu antywirus, firewall czy zablokowany port, po którym próbujesz się łączyć.

    • Dziecko We Mgle

      Czyli na drugim komputerze mam wpisać IP serwera, tak? Dobrze gdyby IP wyświetlało się po kliknięciu LAN Host. Może udałoby się coś takiego zrobić w jednym z najbliższych poradników?

    • Dokładnie tak. Uzyskać IP serwera możesz poprzed linię komend w Windows. Wyświetli Ci się po wpisaniu polecenia ipconfig.

      Multiplayer ma masę różnych zagadnień, gdzie każdy liczy na coś innego, ale postaram się to jakoś po ogarniać.

  • Boguś Witek

    error CS8025: Parsing error

    • Boguś Witek

      mój skrypt proszę o szybką odpowiedź http://wklej.to/Oc4lI

    • Adam

      Nie zamykasz klasy na samym końcu skryptu – }

  • Pingback: Unity na GDC 2016 | mWin()

  • Zumer

    Cześć
    Jak tworzę host w unity i chodzę po świecie jest wszystko ok,
    ale jak połączę się klientem na tym samym pc’ cie. Kamera hosta jest tam gdzie clienta i mogę ruszać obiektem hosta, a kamera ani rusz. Z poziomu clienta mogę ruszać drugim obiektem i jego kamerą. Dlaczego tak się dzieje?

    • Wszystko zależy od tego jakie komponenty wykorzystałeś, jak zrobiłeś połączenie z kamerą, jak wygląda Twój kod etc. Bez tej wiedzy, nie jestem w stanie nic powiedzieć.

    • Zumer

      Kamera jest childem kostki, a takto cały kod zrobiony tak jak w poradniku.

    • Możesz spróbować dodać kamerze komponent NetworkChildTransform. (podaję nazwę z pamięci, więc może brzmieć minimalnie inaczej).

  • Laysiks

    Jak stworzyć ten networkmanager ? Bo jeżeli dodam do Cube’a to wywala mi bład ;x

    • Laysiks

      Do tego nie respi mi mojej kostki

  • wojak

    Witam. Chciałem przerobić swojego fps’a na multi ale dobrnąłem do wyświetlania gui i tu stanąłem. Jak wyświetlić osobne gui dla mnie a osobne dla innego gracza? Dodam że do tej pory obiekt z gui był osobnym game object’em.

    • Trochę trudno powiedzieć, bo to zależy jak definiujesz „dla mnie”. Musiałbyś mieć albo inny build gry z innym menu, jeśli chodzi Ci konkretnie o Ciebie jako Ty. Jeśli jako siebie rozumiesz konkretne IP, możesz chyba sprawdzać IP i wyświetlać inne GUI w zależności od tego IP.

    • wojak

      Hmm… raczej o ip. Bo wyraziłem się dosyć nie szczegółowo. Rzecz w tym abym ja miał wyświetlany swój stan amunicji i zdrowia, a ktoś inny jego. Jeśli wyświetlam gui ze swoim statusem to jest to widoczne tylko dla mnie. Myślę teraz że może osobne warstwy dla każdego gracza, choć raczej nie byłoby to najoptymalniejsze rozwiązanie.

    • Zauważ w jaki sposób przekazujemy tutaj informację o położeniu gracza. Stan amunicji to taka sama zmienna jak transform.position. No dobra, pozycja to Vector3, stan amunicji to int. Ale zasada ich przekazania będzie dokładnie taka sama.

      Sprytne operowanie [SyncVar] i isLocalPlayer, załatwi Ci odpowiedni stan amunicji dla każdego gracza. Mało tego, nie musisz nawet synchronizować wartości. Serwer nie musi wiedzieć ile każdy gracz ma amunicji. Czyli do wyświetlenia danemu graczowi lokalnie amunicji starczy Ci coś takiego:

      int ammo = 20;

      OnGUI() {
      if(isLocalPlayer) {
      GUI.Label(new Rect(0, 0, 100, 20), „Amunicja: ” + ammo);
      }
      }

      Pisane z pamięci i bez sprawdzania, to mogą być jakieś niedomknięte nawiasy, ale najważniejsza jest idea.

    • wojak

      Czyli mam to wyświetlać z komponentu gracza? Bo przy globalnym obiekcie nie wiem który z obiektów prezentuje moją postać. Czy może w jakiś sposób mogę to sprawdzić?

    • Sam UNet Ci to rozróżnia. Każdy model postaci sieciowej posiada komponent NetworkIdentity. On informuje UNet, czy dany obiekt to postać klienta czy serwera.

      Dzięki temu jak wykorzystasz isLocalPlayer, zwróci Ci true, tylko jeśli skrypt wykonuje się na lokalnej maszynie.

      Więc skrypt, który CI podałem, jeśli wrzucisz np. do obiektu gracza, to wykona się jednorazowo tylko dla danego gracza. Po prostu wrzuć sobie to i zobacz jak działa.

    • wojak

      Dzięki problem rozwiązany :)

  • kubastick

    Mam pytanie.
    Czy moge obliczać zmienną na komputerze klienta i tylko ją sunchronizować z serverem?

    • Jasne. Większość rzeczy tak się odbywa. Przykładowo przesuwając postać, najpierw na komputerze klienckim obliczasz nową pozycję i przesyłasz ja do serwera.

  • @kuba_root

    Mam pytanie, czy można użyć ludka z character’ów do tego

  • Bartek Burzyk

    Przepraszam jak jestem idiotą ale nie rozumiem jak jeśli tu jest pokazane chodzenie a jeśli mam swoje własne plus system fps czyli strzelanie kucanie etc to jak mam to zrobic aby to bylo ok pod multi pozdrawiam

    • Problem jest taki, że nie bardzo pomogę pomóc, bo wszystko zależy od tego jak masz napisane swoje skrypty. Poradnik pisałem tak, żeby można z niego było wyłapać te podstawy i zastosować je jakoś w swoich projektach. Zasadniczo nie ma jednej uniwersalnej odpowiedzi na Twoje pytanie.

  • TheStrongSkye

    Moja Kosta znika po naciśnieciu play, w inspectorze jest wyłączona, jak próbuję ją włączyć spowrotem to dalej się nie pokazuje pomimo tego że jest włączona

    • A połączyłeś się z serwerem? Bez tego kostka się nie pojawi.

  • Adrian Turkowski

    gdzie dodać [SyncVar] CubeState state;

    • Tam gdzie znajduje się cała reszta zmiennych. Wewnątrz klasy, ale nie w funkcji.

    • adwadwa

      ja dalej nw gdzie to wstawić

    • adwadwa

      możesz pokazać gdzie w kodzie to wpisać

    • Pokaż swój kod, to Ci powiem, na ogólnikach dokładniej się nie da opisać. :) Ale jeżeli masz z tym problem, to polecam zapoznać się z moim poradnikiem dotyczącym C#.

    • adwadwa

      Juz sobie z tym poradziłem :) wgl świetne poradniki tu piszesz/piszecie ;) jedno pytanie mam te kostki pojawiają mi się w pozycji x y z = 0 jak ustawić by podczas wlanczania pojawiały się w pozycji np x 11 y 12 z 9

    • adwadwa

      ok dałem rade juz :D

    • adwadwa

      proszę o szybka odpowiedz

  • Agentbarti

    Mam Problem jeżeli w silniku unity klikne LAN Host(H) a później na buildzie gry to wtedy klikam LAN Client(C) I wpisuje tam 7777 to mi wyskakuje Connecting To 7777 i po pewnym czasie po prostu znika i nic się nie dzieje ale nadal jest menu ale się nie połączyło z silnikiem Unity

    Proszę o szybką odpowiedź.

    • Ale gdzie wpisujesz 7777? Jeżeli wprowadzasz je zamiast localhost, to nie zadziała. Musi zostać localhost.

  • Adrian Turkowski

    Jak zrobić aby kamera poruszała się z tym blokiem, dodałem ją do tego obiektu ale i ta nie działa

    • Adrian Turkowski

      już się udało, fajny poradnik