wtorek, 1 marca 2016

Tworzenie skryptu generującego planszę

Ten wpis będzie poświęcony tworzeniu planszy do gry i proces ten będę starał się opisać od podstaw w taki sposób, aby był zrozumiały także dla osób, które dotychczas nie miały do czynienia z programowaniem. Na dole tego wpisu zamieszczam link do omawianego tutaj kodu.

Przygotowanie sceny

Na początku uruchamiamy Unity i tworzymy nowy projekt.


Program oferuje nam wybór pomiędzy wersją 3D i 2D, ale te opcje różnią się tylko opcjami kamery, które można przestawić nawet w trakcie tworzenia gry, co sprawia że wybór ten jest odwracalny. Ja w swoim obiekcie będę korzystał z wersji 3D, która doda do gry troszkę głębi. Kolejną opcją która rzuca się w oczy jest możliwość dodania do gry gotowych paczek assetów, ale takie działanie także możemy wykonać w trakcie pracy nad projektem. Ja dotąd nie korzystałem z takich materiałów, uważam że warto jest nauczyć się samemu przygotowywać materiały do gry.

Po utworzeniu projektu ukaże się nam scena, która powinna zawierać kamerę i źródło światła. Obiekty te powinny być wyświetlane w oknie hierarchii:
Od razu dodam, że takie okna można dowolnie przestawiać, dzięki czemu można dostosować program do siebie. Scenę polecam od razu zapisać, oraz zapisywać ją za każdym razem gdy skończymy modyfikować znajdujące się na scenie obiekty, dzięki czemu nie utracimy naszego czasu w razie problemów technicznych.

Gdy przygotowaliśmy scenę do pracy, nareszcie możemy przejść do podstawowych rzeczy, a dokładniej przygotowaniem samej planszy do gry. Można to wykonać na dwa sposoby:
a) Ustawiając i modyfikując obiekty za pomocą edytora Unity.
b) Ustawiając i modyfikując obiekty za pomocą skryptów.
Zaletami pierwszej opcji jest to, że na bieżąco widzimy efekty swojej pracy, oraz nie potrzebujemy żadnych umiejętności związanych z programowaniem. Druga opcja ma nieco więcej zalet: pozwala generować obiekty za pomocą własnych schematów (co mocno ułatwia tworzenie obiektów o regularnych odstępach), pozwala na szybkie wprowadzanie zmian, oraz od razu wrzuca informacje o obiektach do naszego skryptu. Wykonamy więc to drugą metodą.

Najpierw musimy utworzyć skrypt. Aby tego dokonać musimy wybrać w menu Assets → Create → C# Script. Nowy skrypt pojawi się w oknie projektu. Nadajemy jemu nazwę i otwieramy go. Ukaże się nam podstawowy szablon tego kodu:
O czym trzeba pamiętać: nazwa klasy musi być taka sama, jak nazwa skryptu, jeśli więc zmienimy nazwę skryptu, musimy zmienić też nazwę klasy.

Elementem na który zwrócimy tutaj uwagę jest funkcja „void Start ()”. Jest to funkcja która jest automatycznie uruchamiana w trakcie pierwszego uruchomienia się skryptu, czyli jeśli przypiszemy ten skrypt do obiektu znajdującego się w edytorze, to ta funkcja uruchomi się przy starcie gry (ponieważ obiekt pojawia się na starcie gry i jest wtedy do niego przypisywany skrypt). Jest to więc funkcja która nada się do generowania planszy. Inną funkcją która może spełniać taką rolę jest funkcja „void Awake ()”, która uruchamia się gdy skrypt zostanie przypisany do obiektu, czyli nawet przed funkcją Start. W małych projektach wybór między tymi dwoma funkcjami nie ma dużego znaczenia, ale w przypadku generowania planszy funkcja Awake wydaje się być lepszym pomysłem.


Treść funkcji znajduje się między jej klamrami i tylko ten fragment skryptu będzie wykonany przez funkcję. Jeśli postawimy gdzieś dwa ukośniki „//”, to fragment kodu po ich prawej stronie będzie ignorowany przez program. Jest to tzw. komentarz, który służy głównie do zapisywania notatek w kodzie.


Tworzenie obiektu

Najpierw zajmiemy się stworzeniem planszy pełniącą rolę tła. Obiekty można tworzyć za pomocą tzw. prefabów (które są obiektami zapisanymi w materiałach gry), albo za pomocą specjalnych funkcji. Prefaby są znacznie lepszym wyborem w przypadku tworzenia złożonych obiektów (jak np. postacie z gotowymi modelami, teksturami i skryptami), ale w przypadku prostych obiektów równie dobrze można posłużyć się samym kodem. Przykładowo aby stworzyć kwadrat wystarczy jedna prosta linijka kodu:

GameObject.CreatePrimitive (PrimitiveType.Quad);

W kodzie wszystko jest zapisywane od ogółu, do szczegółu, co wyjaśnię na podstawie tej linijki kodu. Pierwsze do czego musimy się odwołać, to klasy „GameObject”, która zawiera w sobie różne funkcje związane z obiektami w grze. Następnie chcemy się odwołać do konkretnej funkcji, czyli do funkcji „CreatePrimitive”, służącej do tworzenia prostych obiektów, a w nawiasie wpisujemy parametr, czyli w tym przypadku typ prostego obiektu, który chcemy stworzyć. Całe polecenie kończymy średnikiem. My jednak chcemy nie tylko stworzyć obiekt, lecz także go modyfikować, a to staje się łatwiejsze, jeśli przypiszemy ten obiekt do zmiennej.

GameObject Clone = GameObject.CreatePrimitive (PrimitiveType.Quad);

Tworzymy więc zmienną typu „GameObject”, a następnie nadajemy jej jakąś prostą nazwę, jak np. „Clone”. Od teraz za każdym razem gdy w tej funkcji użyjemy nazwę naszego obiektu, to program będzie ją traktował jako odwołanie do obiektu. Znak równości w tej linijce kodu oznacza, że to co jest po lewej staje się tym, co jest po prawej.

Modyfikacja obiektu

Skoro stworzyliśmy obiekt, możemy go modyfikować. Pierwszą rzeczą którą chcemy zmodyfikować jest „ojciec” obiektu:

Clone.transform.parent = transform;

„Transform” jest tutaj komponentem zawierającym podstawowe informacje dotyczące obiektu. Jeśli przed wyrażeniem „transform” nie znajduje się żaden obiekt (co ma miejsce po prawej stronie znaku równości), to program odwołuje się do obiektu posiadającego ten skrypt. Jak w prosty sposób zrozumieć czym w kontekście programowania jest „ojciec”? Ojciec w Unity jest jak np. autobus w którym zdarza nam się jeździć w życiu codziennym. Jeśli autobus zacznie się przemieszczać, my przemieszczamy się razem z nim, a jeśli zaczniemy chodzić po autobusie, to przemieszczamy się relatywnie względem niego, a nie względem świata. Gdy wyjdziemy z autobusu, przestaje on być naszym ojcem i zaczyna nim być ziemia. Warto pamiętać, że jeśli w Unity jakiś obiekt nie posiada ojca, to wtedy scena jest traktowana jako jego ojciec.

Kolejną rzeczą jaką chcemy wykonać jest ustalenie jakiejś konkretnej pozycji obiektu. Tak wyglądałoby ustawienie naszego obiektu w punkcie (0, 0, 0) względem naszego ojca:

Clone.transform.localPosition = new Vector3 (0, 0, 0);

Pozycję obiektu można przedstawiać za pomocą zmiennej „position” lub „localPosition”. Pierwsza zmienna odpowiada za pozycję globalną (względem środka sceny), zaś druga pozycja odpowiada za pozycję lokalną (względem tzw. „ojca”). Pozycje obiektów są zapisywane za pomocą wektorów o 3 współrzędnych (x, y, z) i dlatego utworzyliśmy nowy „Vector3”. Standardowo „x” oznacza szerokość, „y” oznacza wysokość, a „z” oznacza głębokość.

Ostatnią rzeczą jaką chcemy zmodyfikować jest skala obiektu:

Clone.transform.localScale = new Vector3 (20, 10, 1);

Wszystko przebiega tak samo jak w przypadku modyfikacji pozycji obiektu. Każda z współrzędnych powinna być większa od zera, jeśli chcemy, aby nasz obiekt był widoczny. Co warto wiedzieć: jeśli skala ojca obiektu zostanie zwiększona, to automatycznie zostanie zwiększona skala obiektu.

Cztery linijki kodu i mamy utworzony prostokąt o pożądanych przez nas właściwościach.

Tworzenie i modyfikacja serii obiektów
Tym razem zajmiemy się tworzeniem planszy o wymiarach 6x6 składającej się z kwadratowych pól. Aby tego dokonać zastosujemy pętlę "for":

for (int x = 0; x < 6; x++) {
}

Przed pierwszym średnikiem następuje deklaracja zmiennej całkowitej (int) o nazwie „x”, której przypisujemy wartość zero. Dzieje się to przy starcie pętli. Następny fragment nawiasu mówi nam, że pętla będzie się wykonywała tak długo, jak długo x będzie mniejszy od 6, czyli jest to instrukcja warunkowa. Ostatni fragment nawiasu mówi nam, że x zostaje zwiększone o 1 za każdym razem, gdy wykonane zostanie jedno przejście pętli. „x++” może też być zapisane jako „x = x + 1” (co oznacza, że x staje się równe wartości „x + 1”), albo „x += 1” (co oznacza, że x zostaje zwiększone o 1). Każdy zapis oznacza właściwie to samo, ale każdy woli to zapisywać na swój sposób.

Dzięki pętli jesteśmy w stanie wykonać tą samą instrukcję kilka razy. Aby ułatwić nam zrobienie planszy o wymiarach 6x6 warto jest umieścić w pętli kolejną pętlę:

for (int x = 0; x < 6; x++) {
 for (int y = 0; y < 6; y++) {
 }
}

A następnie możemy zamieścić wewnątrz fragment kodu odpowiedzialny za tworzenie pól:

for (int x = 0; x < 6; x++) {
 for (int y = 0; y < 6; y++) {
  // Tworzenie kwadratu i przypisanie go do zmiennej
  Clone = GameObject.CreatePrimitive (PrimitiveType.Quad);
  // Ustalenie ojca
  Clone.transform.parent = transform;
  // Ustalenie lokalnej pozycji
  Clone.transform.localPosition = new Vector3 (-2.5f + x, -1.5f + y, -0.01f);
  // Ustalenie skali
  Clone.transform.localScale = new Vector3 (0.9f, 0.9f, 1);
  // Ustalenie koloru
  Clone.GetComponent<Renderer> ().material.color = new Color (0.3f, 0.3f, 0.3f);
 }
}

Łatwo można zauważyć, że ten kod różni się nieco od poprzedniego. Pierwszą różnicą jest dodanie przeze mnie komentarzy – nie mają one wpływu na działanie programu, ale mogą one pomóc niektórym łatwiej orientować się w kodzie. Drugą różnicą jest korzystanie z liczb zmiennoprzecinkowych typu float, które zapisuje się korzystając z kropki oraz dodając na końcu literę „f” (np. -2.5f). Kolejną różnicą jest ustalenie zależności pomiędzy pozycją obiektów, a zmienną odpowiadającą za sterowanie pętlą – dzięki czemu pętle pozwalają nam na tworzenie pól rzędami i kolumnami. Dodatkowo zmieniłem też pozycję względem współrzędnej „z”, aby pola znajdowały się bliżej kamery niż tło, dzięki czemu będziemy mieć pewność, że będą widoczne. Ostatnią różnicą jest zmodyfikowanie koloru obiektu, aby był innego koloru niż tło i był dzięki temu widoczny. Dokonałem tego odwołując się do komponentu „Renderer”, by następnie odwołać się do materiału i jego koloru. Kolor można wyrażać za pomocą RGB (bez przeźroczystości), lub RGBA (z przeźroczystością), sam typ zapisu koloru jest ustalany na podstawie liczby argumentów podanych w nawiasie. Wartości kolorów są standardowo wyrażane od 0 do 1, gdzie 0 jest brakiem określonego kolor, a 1 wynosi 100% tego koloru. Warto jeszcze pamiętać, że nie każdy materiał obsługuje kanał alfa (przeźroczystość), więc jeśli chcemy utworzyć częściowo przeźroczysty obiekt, musimy mu najpierw ustalić odpowiedni materiał.

Przypisywanie skryptu to obiektu w grze

Skoro już stworzyliśmy skrypt generujący planszę wokół jakiegoś obiektu, to nadszedł czas, aby go do jakiegoś obiektu przypisać. Najpierw stwórzmy jakiś obiekt w edytorze unity wchodząc w GameObject → Create Empty. Gdy obiekt jest zaznaczony, w inspektorze wyświetlane są wszystkie jego komponenty i część właściwości:
Skrypt można do niego dodać na 2 sposoby:
a) Przeciągając skrypt z okna projektu na przycisk „Add Component”.
b) Wchodząc w Add Component → Scripts i wybierając utworzony przez nas skrypt.

Po uruchomieniu gry plansza zostanie wygenerowana tam, gdzie znajduje się utworzony obiekt. Teraz pozostaje tylko ustawić odpowiednio kamerę i przetestować efekt.

Link do skryptu:

Brak komentarzy:

Prześlij komentarz