czwartek, 31 marca 2016

Zmienne statyczne

Opadanie żetonów

Gdy ostatnio pisałem o zmianach w tworzeniu żetonów zapomniałem omówić 2 elementy: żetony pojawiają się teraz wysoko nad planszą (poza widokiem kamery) i przypisywany jest do nich stworzony przeze mnie skrypt który sprawia, że opadają one na planszę. Spadające żetony sprawiają, że gra wydaje się być odrobinę żywsza, choć nie ma to wpływu na samą mechanikę rozgrywki. Zawartość skryptu:

using UnityEngine;
using System.Collections;

public class FallingScript : MonoBehaviour {

 void Update () {

  if (transform.localPosition.z < -0.01f) {
   transform.Translate (0, 0, Time.deltaTime * 7.5f / 0.25f);
  } 

  if (transform.localPosition.z >= -0.01f) {
   transform.localPosition = new Vector3 (0, 0, -0.01f);
   Destroy (GetComponent<FallingScript> ());
  }
 }

}

Funkcja Update jest funkcją która aktywuje się automatycznie co klatkę gry, a więc jest wykonywana cały czas. Pierwszą rzeczą którą wykonujemy w tej funkcji jest sprawdzenie, czy pozycja lokalna (względem ojca obiektu) na osi z jest mniejsza od -0.01f (im mniejsza wartość, tym bardziej obiekt jest oddalony od planszy w kierunku kamery). Jeśli warunek if jest prawdziwy, to oznacza, że żeton jest nad planszą i musi dalej przemieszczać się w jej kierunku. Z pomocą przychodzi nam funkcja transform.Translate, która do lokalnej pozycji obiektu dodaje wartości podanych przez nas argumentów - jeśli tylko trzeci argument będzie niezerowy, to obiekt przesunie się względem osi z. Warto tu pamiętać, że odstęp pomiędzy klatkami bywa różny - jeśli jakiś gracz ma 2x mniej klatek na sekundę niż drugi gracz, to u takiej osoby funkcja update będzie się wykonywała 2x rzadziej. Aby żetony spadały z tą samą szybkością u wszystkich graczy korzystamy z funkcji Time.deltaTime, która zwraca czas pomiędzy klatkami - jeśli jakiś gracz będzie miał opóźnienia, to dzięki tej funkcji obiekt przesunie się o większą wartość. Do tego czas pomiędzy klatkami mnożymy przez drogę którą żeton ma pokonać, oraz dzielimy przez czas w którym obiekt ten ma pokonać tą odległość. W rezultacie czas opadania żetonu będzie zawsze taki sam.

Drugi warunek sprawdza, czy lokalna pozycja na osi z jest większa od -0.01f, co oznaczałoby, że żeton dotarł już do planszy, lub znalazł się pod nią. Jeśli do tego dojdzie, to lokalna pozycja obiektu jest ustalana na konkretną wartość, aby mieć pewność, że będzie znajdował się minimalnie nad polem, a następnie nasz skrypt zostaje usunięty z obiektu, pozbywając się zbędnego balastu.

Tutaj pojawia się jednak kolejne przemyślenie: droga którą musi pokonać żeton będzie wykorzystywana w różnych miejscach kodu, np. w trakcie tworzenia żetonów. Podobnie wygląda sytuacja z czasem opadania żetonów: ich umiejętności będą aktywowane dopiero wtedy, gdy opadną na planszę. Gdybyśmy teoretycznie użyli tej wartości w 100 miejscach, a następnie doszlibyśmy do wniosku, że lepsza byłaby minimalnie inna wartość, to stracilibyśmy relatywnie sporo czasu poprawiając nasz kod. Aby temu zapobiec warto jest stworzyć zmienne, do których będziemy mogli odwołać się ze wszystkich skryptów.

Zmienne statyczne

Zmienne statyczne są to zmienne do których możemy się odwołać w każdej chwili z dowolnej sceny, dowolnego obiektu i dowolnego skryptu. Aby to zrobić, musimy tylko podać nazwę klasy w której się ta zmienna znajduje, a po kropce napisać nazwę tej zmiennej. Od razu można zauważyć, że jest to wygodniejsze niż odwoływanie się do zmiennych niestatycznych, ponieważ nie musimy podawać nazwy obiektu w którym znajduje się skrypt z tą zmienną. Dzieje się tak dlatego, że zmienna ta jest wspólna dla wszystkich instancji tej klasy. Co więcej nasz skrypt może nie być przypisany do żadnego obiektu na scenie a i tak będziemy mieli dostęp do jego zmiennych statycznych. Warto też pamiętać, że zmienne te są deklarowane przy starcie programu i pozostają niezmienione podczas zmiany sceny. To wszystko sprawia, że zmienne statyczne są okropnym narzędziem do przechowywania informacji o stanie rozgrywki (wyobraźcie sobie, że w jakiejś grze wy i wasi przeciwnicy macie wspólne zdrowie, które nie odnawia się po ponownym wczytaniu rozgrywki), ale za to mogą świetnie się spisywać w roli ustawień gry (np. głośności muzyki).

Utworzyłem więc nowy skrypt:

 using UnityEngine;
 using System.Collections;

 public class GameData : MonoBehaviour {

  static public float FallingDuration = 0.25f;
  static public float FallingDiestance = 7.5f;
 }


Zmienne statyczne można tu rozpoznać po wyrazie static znajdującym się przed deklaracją zmiennej. Skoro dodałem do gry dwie nowe zmienne, mogę wstawić je we wcześniej wymienionym skrypcie zamiast stałych wartości:

  transform.Translate (0, 0, Time.deltaTime * GameData.FallingDiestance / GameData.FallingDuration);

Stworzenie skryptu z danymi/opcjami gry jest świetną okazją, aby przenieść do niego informacje o żetonach o których pisałem kilka wpisów temu. Jest ku temu kilka dobrych powodów:
- informacje te są zawsze takie same,
- chcemy mieć do nich dostęp z wielu skryptów i wielu scen (nie tylko ze scen online klienta i serwera, ale także z poziomu menu),
- nie ma potrzeby wczytywania tych samych informacji kilkukrotnie w trakcie działania programu.

Tutaj pojawia się jednak nowy problem: dotychczas jeśli chcieliśmy, aby jakiś fragment kodu był wykonany tylko na starcie czegoś, to umieszczaliśmy go w funkcji Start, a cały skrypt był doczepiany do jakiegoś obiektu na scenie. My jednak chcemy, aby skrypt nie był doczepiany do żadnego obiektu. W takiej sytuacji pomocny będzie tzw. konstruktor, czyli funkcja która wywołuje się automatycznie po utworzeniu się klasy, w naszym przypadku konstruktor statyczny będzie się wykonywał tylko podczas wczytywania gry. Aby zadeklarować konstruktor statyczny, musimy najpierw napisać wyraz static, a następnie musimy napisać nazwę klasy w której ten konstruktor się znajduje. Przykład:

 using UnityEngine;
 using System.Collections;

 public class GameData : MonoBehaviour {

  static public float FallingDuration = 5.25f;
  static public float FallingDiestance = 7.5f;

  static GameData() {
   FallingDuration = 0.25f;
  }
 }


Po uruchomieniu gry wartość zmiennej FallingDuration będzie ustawiona na 0.25f, mimo iż przy deklaracji zmiennej była podana inna wartość, co oznacza, że nasz konstruktor zadziałał. Jeśli ktoś chciałby się upewnić, że uruchomi się on tylko raz, to można dodać do klasy zmienną statyczną typu int o wartości 0, którą będziemy zwiększać o 1 w konstruktorze. Bez względu na to ile razy zmienimy scenę w grze wartość tego inta będzie zawsze wynosiła 1, co rozwieje wszelkie wątpliwości.

wtorek, 29 marca 2016

Dostosowywanie gry do trybu multi

Na początku trochę o rozłożeniu skryptu na obiektach. Dotychczas miałem w nawyku trzymanie możliwie jak największej liczby informacji w jednym skrypcie, co nie przeszkadzało w grach tworzonych z myślą o single player. Przy zabawia z trybem multi doszedłem do wniosku, że najlepiej będzie, gdy informacje związane ze sceną będą przechowywane tylko na kliencie, co da lekką ulgę serwerowi. PlayerPrefab będzie natomiast zawierał skrypty związane z komunikacją między klientem a sceną. Skrypt umożliwiający wysyłanie serwerowi poleceń musi istnieć zarówno na obiekcie klienta, jak i odpowiedniku tego obiektu na serwerze i dlatego starałem się podzielić główny skrypt na dwie części. Sam schemat będzie wyglądać mniej więcej tak:

Scena


Chcemy mieć dwie sceny: scenę offline i scenę online. Na obu scenach będzie znajdować się kamera i źródło światła, na scenie offline będzie znajdował się obiekt ze skryptem NetworkManager, a na scenie online będzie znajdować obiekt ze skryptem NetworkIdentity z zaznaczą opcją ServerOnly (będzie on służył do przechowywania danych o sytuacji na planszy). Po podłączeniu się na serwer online obiekt ze skryptem NetworkManager przeniesie się ze sceny offline, wiec nie trzeba go tam umieszczać.

Musimy mieć jakiś prefab który umieścimy w skrypcie NetowrkManager jako PlayerPrefab. Do tego prefabu dodałem następujący skrypt:

 using UnityEngine;
 using System.Collections;
 using UnityEngine.Networking;

 public class PlayerScript : NetworkBehaviour {

  NetworkIdentity MyNet;

  void Start () {
   if (isLocalPlayer) {
    name = "MyPlayer";
    MyNet = GetComponent<NetworkIdentity> ();
    Instantiate (Resources.Load ("PreMObject"));
   }
  }

 }

Skrypt ten ma dodaną bibliotekę związaną z sieciami. Skrypt po uruchomieniu odpala funkcję Start, która najpierw sprawdza, czy PlayerPrefab jest naszą własnością - po podłączeniu się innych graczy na naszej scenie pojawi się ich PlayerPrefab, a my nie chcemy aby ten fragment kodu aktywował się na nie-naszych obiektach. Jeśli obiekt jest naszą własnością, to zmieniamy jego nazwę, abyśmy mogli łatwo go znaleźć na scenie. Drugą rzeczą jest przypisanie do zmiennej informacji o tym jaki konkretnie skrypt NetworkIdentity jest do niego przyczepiony. Zmienną tą będziemy czasem przekazywać na serwer, aby móc ją odebrać z pozostałych klientów. Ostatnią rzeczą robioną przez ten fragment kodu jest utworzenie instancji prefabu o nazwie "PreMObject", który utworzyłem przedtem w folderze Resorces. Chciałem aby obiekt ten był tworzony za pośrednictwem skryptu aby mieć pewność, że uruchamiane przez niego funkcje zawsze uruchomią się po pojawianiu się PlayerPrefabu. Gdyby kolejność była inna, to niektóre moje skrypty nie mogłyby odnaleźć naszego gracza i nie zadziałałyby poprawnie.

Mój prefab "PreMObject" zawiera obiekt ze skryptem zajmującym się sceną na kliencie. Ze starego skryptu zostawiłem fragmenty kodu związane z planszą i prócz tego na początku funkcji start dodałem linijkę kodu zmieniającą nazwę obiektu:

 name = "MObject";

Skrypt po staremu tworzy całą planszę, przechowuje o niej informacje i przypisuje do pól skrypt umożliwiający sterowanie, który trzeba nieco zmodyfikować.


Skrypt sterowania


Pierwszą zmianą jest dodanie zmiennej wskazującą na skrypt związany z komunikacją z serwerem, gdzie za typ zmiennej podstawiamy nazwę tamtego skryptu:

 public PlayerScript PScript;

Następnie w funkcji Start dodałem linijkę kodu przypisujący skrypt do tej zmiennej:

 PScript = GameObject.Find ("MyPlayer").GetComponent<PlayerScript> ();

I przekształciłem linijkę kodu wywołującą funkcję tworzącą żeton, aby była ona wywoływana ze wspomnianego skryptu:

 StartCoroutine (PScript.UseToken (x, y));

Komunikacja z serwerem


Teraz powracamy do skryptu PlayerScript. Chcemy aby informacja o postawieniu żetonu była wysłana na serwer, by ten rozkazał klientom, aby utworzyli żeton. Jeśli chcemy wydać serwerowi polecenie z poziomu klienta, musimy zastosować atrybut command. Aby dodać ten atrybut trzeba przed funkcją napisać "[Command]", a za trzy pierwsze znaki nazwy tej funkcji musimy wstawić "Cmd", tak jak na przykładzie:

 [Command]
 public void CmdUseToken (int x, int y) {

 }

Z kolei jeśli z poziomu serwera chcemy wydać polecenie klientom, musimy zastosować atrybut [ClientRpc], a trzema pierwszymi znakami nazwy funkcji muszą być znaki "Rpc", tak jak na przykładzie:

 [ClientRpc]
 public void RpcSpawnToken (int x, int y) {

 }

Z taką wiedzą uzupełniamy skrypt PlayerScript o następujący fragment kodu:

 public IEnumerator UseToken (int x, int y) {
  CmdUseToken (x, y);
  yield return new WaitForSeconds (0.25f);
 }
 [Command]
 public void CmdUseToken (int x, int y) {
  RpcSpawnToken (x, y);
 }
 [ClientRpc]
 public void RpcSpawnToken (int x, int y) {
  MScript MScript = GameObject.Find ("MObject").GetComponent<MScript> ();
  GameObject Clone = Instantiate (Resources.Load ("PreToken")) as GameObject;
  MScript.Token [x, y] = Clone;
  Clone.transform.parent = MScript.Field [x, y].transform;
  Clone.transform.localPosition = new Vector3 (0, 0, -7.5f);
  Clone.transform.localScale = new Vector3 (0.9f, 0.9f, 1);
  Clone.AddComponent<FallingScript> ();
  if (MyNet == GetComponent<NetworkIdentity> ()) {
   Clone.GetComponent<Renderer> ().material.color = new Color (0.65f, 0.9f, 0.55f);
  } else {
   Clone.GetComponent<Renderer> ().material.color = new Color (0.85f, 0.6f, 0.6f);
  }
  Clone = Instantiate (Resources.Load ("PreText")) as GameObject;
  Clone.transform.parent = MScript.Token [x, y].transform;
  Clone.transform.localPosition = new Vector3 (0, 0, -0.01f);
  Clone.transform.localScale = new Vector3 (0.9f, 0.9f, 1);
  Clone.GetComponent<TextMesh> ().text = "2";

}

Ostatnia funkcja wyszukuje na kliencie skrypt z informacjami o scenie, aby móc przypisać do niego informacje o żetonie który tworzy. Warto tu zwrócić uwagę, że funkcja ta będzie wykonywana na każdym kliencie i na każdym kliencie ten skrypt jest wyszukiwany osobno. Kolejną rzeczą której przedtem nie było jest podmiana koloru żetonu: jeśli komponent NetworkIdentity gracza który rozkazał postawienie żetonu jest taki sam co NetworkIdentity klienta, to znaczy że tworzymy żeton na własnym kliencie i wtedy ma kolor zielony, w przeciwnym razie ma kolor czerwony. Wartość koloru jest wyrażana w wartości RGB. Do tego jeszcze dochodzi fragment kodu tworzący tekst wyświetlany nad żetonem.

Teraz możemy już stawiać żetony na różnych klientach i pojawiają się one na każdym z klientów.

niedziela, 27 marca 2016

Sceny na klientach

Dzisiejszy wpis będzie poświęcony zawartości sceny na różnych klientach.

Wszystkie obiekty które zapisaliśmy na scenie są standardowo wczytywane u każdego klienta, który tą scenę wczyta. Nie oznacza to jednak, że te obiekty są wspólne dla wszystkich klientów, wręcz przeciwnie - każdy klient posiada osobną scenę i każdy klient wczytuje ją osobno. Jeśli na scenie znajdowałby się skrypt generujący obiekty o losowych pozycjach, to prawie zawsze ta sama scena wyglądałaby inaczej na każdym z klientów. Podobną sytuację zaobserwujemy, jeśli posiadamy skrypt tworzący obiekty po kliknięciu lpm - obiekty utworzone na jednym kliencie nie będą tworzone na pozostałych klientach.

Jeśli sieć nie ma dedykowanego serwera, to rolę serwera przejmuje jeden z klientów, tzw. host.

Na scenie hosta znajdują się zarówno obiekty które powinny znajdować się na jego kliencie, jak i obiekty znajdujące się na serwerze, które niestety są niewidoczne na pozostałych klientach. Momentami może to być kłopotliwe dla nowych programistów, ponieważ podczas testowania gry na klientach zdalnych (innych niż host) może się okazać, że nie działają tam rzeczy testowane dotychczas na hoście.

Jeśli chcemy, aby jakiś obiekt miał odblokowane różne opcje związane z sieciami, musimy dodać do niego komponent NetworkIdentity. Stwórzmy więc na scenie pusty GameObject, dodajmy do niego ten komponent, a następnie zaznaczmy w nim opcję ServerOnly. Dzięki tej opcji obiekt nie będzie tworzony na żadnym z klientów i zostanie utworzony tylko na serwerze, w chwili rozpoczęcia połączenia. Jest to przydatne jeśli chcemy, aby w całej sieci istniała tylko jedna kopia jakiegoś elementu i była dostępna na serwerze.

Spawn

Utwórzmy teraz nowy skrypt, który wypełnijmy następującym schematem:
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
public class HostScript : NetworkBehaviour {
}
Do standardowego schematu skryptu dodaliśmy nową bibliotekę: UnityEnginie.Networking, która daje nam dostęp do narzędzi związanych z siecią, oraz zamieniliśmy MonoBehaviour na NetworkBehaviour, co także jest dla nas konieczne. Jeśli do jakiegoś obiektu przypiszemy skrypt zawierający te elementy, to automatycznie ten obiekt otrzyma także skrypt NetworkIdentity, co jest całkiem wygodne. Sama nazwa klasy może być dowolna.

Teraz wypełnijmy tą klasę następującą funkcją:
 public override void OnStartServer () {
  NetworkServer.Spawn (Instantiate (Resources.Load ("PreMObject")) as GameObject);
 }
Funkcja ta aktywuje się w chwili startu serwera. Zawiera ona w sobie znaną nam już funkcję Instantiate, która w tym przypadku powoduję utworzenie instancji prefabu o nazwie "PreMObject" znajdującego się w katalogu o nazwie "Resources", a wszystko to zawiera się w funkcji NetworkServer.Spawn (), która może być uruchomiona tylko na serwerze i powoduje utworzenie wybranego prefabu na każdym kliencie. Warto pamiętać, że aby skorzystać z tej opcji nasz prefab musi być dodany do tabeli możliwych do spawnowania obiektów, która znajduje się w skrypcie NetworkManager. Wszystko to sprawi, że nasz obiekt pojawia się na scenie wszystkich klientów i wszystkie te kopie obiektu będą powiązane z serwerem, co w przyszłości pozwoli nam na łatwiejszą wymianę informacji, bowiem Unity oferuje nam możliwość tworzenie zmiennych o atrybucie [SyncVar], o której rozpiszę się później.

Utworzony skrypt przypiszmy do obiektu, który ma znajdować się tylko na serwerze. Uzupełnijmy też folder Resources o potrzebny do tego skryptu prefab, oraz dodajmy go do skryptu NetworkMenager. W rezultacie po uruchomieniu gry obiekt istniejący tylko na serwerze utworzy dla każdego klienta
Mimo iż wszystkie te obiekty są powiązane, to osobnymi obiektami i na każdym kliencie mogą mieć inne właściwości, np. pozycję. W razie potrzeb możemy jednak wysyłać na serwer nowe informacje na temat tego obiektu, lub pobierać takie informacje, więc w zależności od naszych wymagań poszczególne elementy mogą być synchronizowane, lub mogą pozostać unikalne. W wielu grach można to wykorzystać np. do barwienia postaci, gdzie popularnym motywem jest barwienie sojuszniczych postaci na zielono, a dla każdej drużyny inne postacie są uznawane za przyjazne. Inny przykład zastosowania: możemy zredukować częstotliwość komunikowania się z serwerem, aby go nie przeciążać.

Player Prefab

Kolejnym omawianym elementem będzie PlayerPrefab, który możemy wybrać w skrypcie NetworkManager. Prefab ten wymaga komponentu NetworkIdentity i będzie tworzony na serwerze za każdym razem gdy nowy gracz dołączy się do sieci i pojawi się na każdym kliencie, a po rozłączeniu się gracza obiekt ten jest usuwany ze wszystkich klientów.
PlayerPrefab podobnie jak spawnowane obiekty nie jest automatycznie synchronizowany ze swoimi odpowiednikami na innych klientach. Gracz będący właścicielem takiego obiektu posiada nad nim Authority, czyli może wysyłać z niego polecania na serwer, jednak obiekty pozostałych graczy pozostają poza tą kontrolą. W razie potrzeb istnieje też możliwość nadania autorytetu także innym obiektom.

piątek, 25 marca 2016

Network Manager

Na początku przepraszam za dłuższą przerwę, ale ostatnio miałem mało czasu. Postaram się nadrobić te zaległości.

Dzisiaj zajmiemy się przekształcaniem gry w wersję multiplayer. Początkowo może dziwić, że zabieram się za to na tak wczesnym etapie tworzenia gry, ale im wcześniej się za to zabiorę, tym mniej fragmentów kodu trzeba będzie poprawiać. Jeśli tworzy się grę wyłącznie z myślą o trybie single player, a następnie daje się w niej połączenie między graczami, to w rozgrywce nic się nie zmieni - gracze nadal nie posiadaliby ze sobą żadnej interakcji. Działoby się tak dlatego, że napisany przez nas kod nie posiadałby w sobie żadnych poleceń wysyłanych z klienta na serwer i na odwrót.

NetworkManager

Na początku tworzymy na scenie pusty GameObject, klikamy na niego aby zobaczyć jego dane w inspektorze, a następnie dodajemy do niego dwa komponenty, znajdujące się w katalogu Network: NetworkManager i NetworkManagerHUD. Pierwszy z tych skryptów umożliwia nam korzystanie z nowych funkcjonalności Unity związanych z sieciami.



Pierwszą rzeczą którą nas interesuje są zmienne związane ze scenami. Zmienna OfflineScene określa na jakiej scenie będziemy się znajdować, jeśli nie będziemy połączeni z serwerem, a druga zmienna określa na jaką scenę mamy być wysłani, jeśli połączymy się z serwerem. Zazwyczaj rolę sceny offline pełni scena menu, a scena online jest sceną z właściwą grą, ale w przypadku niektórych gier scena z grą jest przypisana do obu tych zmiennych.

Nasze sceny powinny znajdować się w oknie projektu wraz z innymi assetami. Zazwyczaj aby przypisać asset do zmiennej w jakimś skrypcie wystarczy przeciągnąć go z okna projektu do interesującego nas skryptu. Sytuacja komplikuje się w przypadku scen, ponieważ najpierw muszą one być umieszczone w opcjach budowy projektu. Aby je tam wstawić należy wejść w File -> Build Settings..., a następnie dodać tam nasze sceny (co można dokonać np. przeciągając je z okna projektu). Teraz powinniśmy już być w stanie przypisać je także do NetworkManagera. Na razie nie mamy stworzonego żadnego ciekawego menu, więc leniwie przypiszemy tą samą scenę do obu zmiennych. Warto wiedzieć, że jeśli przechodzimy z jakiejś sceny do tej samej sceny, to następuje jej ponowne wczytanie.

Kolejną rzeczą wartą uwagi jest zmienna PlayerPrefab. Jeśli zaznaczymy opcję AutoCreatePlayer, to obiekt ten będzie automatycznie stworzony po podłączeniu się klienta do serwera. Odpowiednik tego obiektu jest tworzony także na wszystkich pozostałych klientach, ale każdy klient posiada autorytet tylko nad swoim własnym PlayerPrefabem i tylko z niego będzie mógł wysyłać polecenia na serwer. Jeśli klient się rozłączy, to jego PlayerPrefab zostanie automatycznie usunięty z serwera i pozostałych klientów.

Następnym interesującym nas elementem jest tablica prefabów RegisteredSpawnablePrefabs. Rozmiar tej tablicy możemy modyfikować za pomocą znaków + i -, a elementy możemy przeciągnąć z okna projektu. Dodanie prefabu do tej tablicy umożliwia nam używanie jego w funkcji Spawn, która powoduje utworzenie tego obiektu na wszystkich klientach. Tą tablicę będziemy stopniowo powiększali w trakcie tworzenia gry.

Skrypt NetworkManagerHUD tworzy w naszej grze proste menu umożliwiające tworzenie serwerów i dołączanie się do serwerów. Na początek takie proste menu powinno nam wystarczyć.

sobota, 12 marca 2016

Stawianie żetonów na planszy

Plan na dziś jest prosty: sprawić, że po kliknięciu na pole pojawi się na nim żeton. Na razie pominiemy możliwość wybrania żetonu i związane z nim animacje – po prostu chcemy postawić żeton.


Wydarzenie wyzwalające

Dotychczas wszystkie czynności programu zaczynały się na jego starcie i nawet można było trzymać cały kod w jednym skrypcie. Jednak teraz idziemy o krok naprzód i chcemy aby żeton tworzył się po kliknięciu na pole, co może zdarzyć się w dowolnym momencie gry. Unity oferuje nam funkcję „OnMouseOver ()”, która aktywuje się za każdym razem, gdy najedziemy myszką na obiekt posiadający skrypt. Pamiętamy jednak, że w grze będziemy mieli do wyboru 36 różnych pól, a więc wygląda na to, że skrypt będzie musiał być przypisany do każdego z nich. Dodatkowo nie możemy przypisać do pól wcześniej utworzonych skryptów, ponieważ miały one uruchomić się jednorazowo przy starcie gry, a jeśli przypiszemy je kilkudziesięciu pól, to aktywują się kilkadziesiąt razy. Dlatego też tworzymy nowy skrypt, który przypiszemy potem do pól.

Najechanie myszką na pole jest zdarzeniem wyzwalającym aktywację kodu, ale potrzebny jest jeszcze jeden warunek: wciśnięty lewy przycisk myszy. Unity oferuje nam 3 wbudowane funkcje typu zmiennej bool:

 Input.GetMouseButton (0) // zwraca prawdę, jeśli przycisk myszy jest przytrzymany
 Input.GetMouseButtonDown (0) // zwraca prawdę, jeśli przycisk myszy został wciśnięty
 Input.GetMouseButtonUp (0) // zwraca prawdę, jeśli przycisk myszy został zwolniony

Pierwszą funkcję tutaj zdecydowanie omijamy, ponieważ przytrzymanie lpm sprawi, że funkcja aktywuje się tyle razy, ile klatek będzie trwało przytrzymanie lpm, a my chcemy, aby aktywowała się tylko raz. Różnica między 2 i 3 funkcją ma spore znaczenie tylko wtedy, gdy chcemy umożliwić przeciąganie obiektów/rozkazów pomiędzy dwoma punktami, więc póki co wybór nie ma tutaj dużego znaczenia. Gdybyśmy chcieli, aby funkcja aktywowała się dla ppm, to wtedy jako argument funkcji wstawiamy 1, a w przypadku środkowego przycisku myszy argumentem funkcji powinna być liczba 2.

Na razie więc funkcja wygląda tak:

 void OnMouseOver () {
  if (Input.GetMouseButtonDown (0)) {
  }
 }

I w środku tej funkcji powinniśmy wstawić skrypt tworzący żeton.

Komunikacja między skryptami

W trakcie tworzenia żetonu chcemy korzystać z danych dotyczących tego żetonu, które znajdują się w innym skrypcie. I tutaj mamy dwie główne możliwości:
a) Pobieramy dane żetonu ze skryptu z danymi do skryptu sterującego.
b) Pobieramy informację o tworzeniu żetonu ze skryptu sterującego do skryptu z danymi.
Danych żetonu może być wiele, natomiast informacja o postawieniu żetonu jest jedna, dlatego wygodniej będzie skorzystać z drugiej opcji. Fragment kodu tworzący żeton umieścimy więc w naszym głównym skrypcie i użyjemy do tego funkcji typu IEnumerator, którą możemy pauzować na dowolną ilość czasu, co przyda nam się później np. do opóźniania efektów graficznych umiejętności. Dla funkcji tej stworzymy dwa argumenty: współrzędną x pola, oraz współrzędną y pola. Funkcja będzie więc wyglądała tak:

 public IEnumerator CreateToken (int x, int y) {
 }

Funkcja ta jest zmienną publiczną, abyśmy mogli się do niej dostać z poziomu innych funkcji. Wróćmy więc do funkcji sterującej, aby dodać tam wywołanie tej funkcji. Funkcję typu IEnumerator
można wywołać komendą StartCoroutine () której argumentem będzie wspomniana funkcja.

Jeśli odwołujemy się to funkcji z innego skryptu, to najpierw musimy odwołać się do jej skryptu, a aby odwołać się do skryptu, musimy wpierw odwołać się do obiektu w którym on się znajduje. Wszystko tutaj dzieje się według reguły „od ogółu do szczegółu”. Z pomocą przychodzi nam funkcja GameObject.Find (), która zwraca obiekt znajdujący się na scenie o nazwie podanej jako argument tej funkcji. Skoro mamy dostęp obiektu to teraz możemy odwołać się do jego komponentu za pomocą funkcji GetComponent<> (), która zwraca komponent o nazwie znajdującej się w nawiasie ostrokątnym, gdzie podamy nazwę naszego skryptu. Jeśli założymy, że skrypt ma nazwę „MScript” i znajduje się w obiekcie „MObject”, to aktywacja naszej funkcji CreateToken () będzie wyglądała następująco:

 StartCoroutine (GameObject.Find ("MObject").GetComponent<MScript> ().CreateToken (x, y));

Jak już wspomniałem, funkcja potrzebuje 3 argumentów, więc w klasie tego skryptu trzeba utworzyć zmienne które będą zawierały potrzebne informacje:

 public int x;
 public int y;

Na razie te zmienne są puste.

Przypisywanie skryptu do pól

Skoro pola na planszy były całkowicie generowane za pomocą głównego skryptu, to i nowy skrypt powinien być dodany w trakcie generowania tych pól. Cofamy się więc i modyfikujemy fragment kodu odpowiedzialny za pola, aby wyglądał następująco:

 for (int x = 0; x < 6; x++) {
  for (int y = 0; y < 6; y++) {
   Clone = GameObject.CreatePrimitive (PrimitiveType.Quad);
   Clone.transform.parent = transform;
   Clone.transform.localPosition = new Vector3 (-2.5f + x, -1.5f + y, -0.01f);
   Clone.transform.localScale = new Vector3 (0.9f, 0.9f, 1);
   Clone.GetComponent<Renderer> ().material.color = new Color (0.3f, 0.3f, 0.3f);
   Clone.AddComponent<ControlScript> ();
   Clone.GetComponent<ControlScript> ().x = x;
   Clone.GetComponent<ControlScript> ().y = y;
   Field [x, y] = Clone;
  }
 }

Zmiany niewielkie: dodano 4 linijki kodu, przy czym 3 pierwsze odpowiedzialne są za skrypt który nazwaliśmy „ControlScript”. Pierwsza linijka dodaje ten skrypt do pola jako jego komponent, natomiast dwie kolejne linijki odwołują się do tego komponentu, by móc zmodyfikować znajdujące się w nim zmienne i ustalić dla nich odpowiednią wartość. Zmienna PNumber pozostaje póki co nienaruszona. Ostatnia linijka odpowiada za przypisanie obiektu to tablicy zmiennych Field, którą dodatkowo musimy zadeklarować w naszej klasie:
 public GameObject [,] Field = new GameObject [6, 6];
Dzięki tej zmiennej w dowolnym momencie będziemy mogli w łatwy sposób odwołać się do dowolnego pola na planszy.

Tworzenie żetonu

Skoro mamy już przygotowane wszystkie dane związane z wywołaniem funkcji tworzącej żeton, to możemy przejść do samego tworzenia żetonu, co dokonamy za pomocą instancji. Najpierw utwórzmy w assetach folder „Resources” do którego możemy łatwo odwołać się z poziomu kodu, a następnie utwórzmy w nim prefab o nazwie „PreToken”, w którym umieścimy nasz żeton. Może on być złożonym modelem 3D, ale równie dobrze może być to zwykły quad z okrągłą teksturą. Jeśli chcemy aby quad miał okrągłą teksturę, najpierw musimy jakąś stworzyć, a następnie ją zaimportować (co można dokonać przeciągając ją do okna z assetami), a gdy już znajdzie się ona w plikach naszej gry, musimy kliknąć na nasz prefab i przeciągnąć nową teksturę na przycisk „Add Component”. Jeśli tekstura przedstawia koło, to oznacza, że korzysta z przeźroczystości, a podstawowy shader w Unity nie obsługuje przeźroczystości. Aby uporać się z tym problemem trzeba zmienić shader na jakiś inny. Jeśli utworzymy na scenie instancję tego prefabu, to w trakcie zmieniania shadera od razu będziemy widzieli jak zmienia się nasza tekstura, więc można szybko znaleźć odpowiadający nam shader. I teraz wracamy do funkcji tworzącej żeton, aby dodać w niej następującą linijkę kodu:

 Token [x, y] = Instantiate (Resources.Load ("PreToken")) as GameObject;

Linijka ta oznacza, że do tablicy zmiennych o nazwie Token przypisujemy instancję prefabu. Mimo iż prefab ma wiele wspólnego z typem GameObject, to nie jest traktowany tak samo, dlatego musimy dodać wyrażenie „as GameObject”, aby móc go przypisać do zmiennej, która nie jest prefabem. Dotychczas nie zadeklarowaliśmy jeszcze zmiennej Token,więc musimy ją zadeklarować:

 public GameObject [,] Token = new GameObject [6, 6];

Dzięki przypisaniu instancji do zmiennej będziemy mogli łatwo odwołać się do niej w przyszłości. Chcemy jeszcze, aby ta instancja pojawiła się tuż nad polem, co można dokonać następującymi linijkami kodu:

 Token [x, y].transform.parent = Field [x, y].transform;
 Token [x, y].transform.localPosition = new Vector3 (0, 0, -0.1f);

W ten sposób pole stanie się ojcem żetonu i pojawi się minimalnie nad nim. Cała funkcja będzie więc wyglądała tak:

 public IEnumerator CreateToken (int x, int y) {
  Token [x, y] = Instantiate (Resources.Load ("PreToken")) as GameObject;
  Token [x, y].transform.parent = Field [x, y].transform;
  Token [x, y].transform.localPosition = new Vector3 (0, 0, -7.5f);
  yield return new WaitForSeconds (0.01f);
 }

Yield odpowiada za pauzę w działaniu funkcji. Na razie nie jest ona pożyteczna, ale przyda się później.