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

Dzisiejszy odcinek: Przemieszczanie obiektu za pomocą myszy

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

Na wstępie zaznaczę ważną rzecz. Niniejszy poradnik jest niejako rozwinięciem poprzedniego tutoriala, który był poświęcony zaznaczaniu obiektów za pomocą myszki. W tym momencie zakładam, że ten poradnik masz przerobiony i rozumiesz jego zawartość, gdyż będzie on dla nas punktem wyjścia do dalszych kroków.

To co dziś sobie zrobimy, to możliwość sterowania postacią przez wskazanie jej miejsca docelowego, za pomocą kliknięcia przycisku myszki. Rozwiązanie znane z gier typu X-Com, Jagged Alliance, czy większości strategii.

Po poprzednim tutorialu mamy postać, którą da się zaznaczyć. Teraz dopiszemy kod, który pomoże sterować zaznaczoną postacią.

Rozwiązania są dwa. Możemy sobie sami napisać skrypt pathfindingu, gdzie najczęściej jest to algorytm A* (A star), ale… Unity dysponuje NavMeshem, który rozwiązuje bardzo dużo spraw za nas. Więc nie będziemy sobie dokładać roboty, tylko wykorzystamy NavMesh.

Przygotowanie

To co będziemy potrzebować, to znacznik celu. Najczęściej jest to jakieś kółko czy inny śmieszny znaczek, który sygnalizuje, gdzie powinna znajdować się postać po zakończeniu przemieszczania się. Nie kombinowałem za bardzo i wykorzystałem okrąg z poprzedniego poradnika, tylko zmieniłem mu kolor. Tutaj dla was za free:

Znacznik celu
Znacznik celu

Zaczniemy od dodania tego obrazka do projektu. Zwyczajnie wystarczy go dodać do plików.

Ustawiamy opcje dla tekstury
Ustawiamy opcje dla tekstury

Najważniejsze, tutaj to ustawić Texture Type na Sprite (2D and UI). Teraz tworzymy sobie materiał, w oknie Project klikamy prawym klawiszem na wolnym obszarze i wybieramy Create -> Material. Do materiału dodajemy naszą teksturę i zmieniamy Shader na Unlit/Transparent, aby otrzymać sam okrąg, bez dodatkowych kolorów.

Ustawienia materiału
Ustawienia materiału

Jeśli na tym etapie, na podglądzie nie masz samego okręgu z przeźroczystością, to prawdopodobnie Twoja grafika ma niewycięte tło, albo jest w złym formacie. Z popularnych formatów GIF i PNG obsługują przeźroczystość.

Czas utworzyć kompletny znacznik. Aby to zrobić, tworzymy nowego plane’a: [GameObject -> 3D Object -> Plane]. Dodajemy do niego nasz materiał (przez przeciągnięcie materiału na obiekt). Następnie tworzymy sobie prefab. Aby to uczynić przeciągamy obiekt z okna Scene, do okna Project. Można usunąć obiekt ze sceny, kiedy prefab utworzył się poprawnie.

Poprawiamy gracza

Gracz, ma korzystać z NavMesha. Dlatego, musimy go uczynić agentem (obiektem, poruszającym się z wykorzystaniem NavMesha). Dlatego zaznaczamy obiekt gracza i wybieramy: [Component -> Navigation -> Nav Mesh Agent]. Parametry komponentu można spokojnie modyfikować, ale jeśli Twoja postać to kapsuła, to domyślne ustawienia będą odpowiednie. Do NavMesha wrócimy później. Teraz przerobimy sobie trochę kod.

Będziemy pracować na skrypcie PlayerController. Na początku dodamy do kodu dodatkowe zmienne:

public GameObject targetBecon;
private NavMeshAgent agent;

Od razu z poziomu Unity, możemy przeciągnąć do pierwszej nasz prefab.

Z racji, że chcemy oszczędzić sobie nieco zasobów, skorzystamy z object poolingu. Tzn. stworzymy sobie raz indykator celu i będziemy go tylko ukrywać i pokazywać, zamiast każdorazowo usuwać i tworzyć od nowa. Dlatego też na starcie, od razu stworzymy sobie nasz indykator:

void Start () {

	(...)

	targetMarker = Instantiate (targetBecon, transform.position, transform.rotation) as GameObject;
	targetMarker.SetActive (false);
	agent = GetComponent<NavMeshAgent> ();
}

Dodatkowo zapisujemy nasz komponent NavMeshAgent, żeby go nie szukać za każdym razem.

Teraz czas na samo przemieszczanie postaci:

void Update () {
	if (Input.GetMouseButtonDown (0) && isSelected) {
		Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
		RaycastHit hitInfo;
		if (Physics.Raycast (ray.origin, ray.direction, out hitInfo)) {
			if (hitInfo.transform.gameObject.tag != "Player" && hitInfo.transform.gameObject.tag != "Enemy") {
				Vector3 targetPosition = new Vector3 (hitInfo.point.x, hitInfo.point.y, hitInfo.point.z);

				targetMarker.transform.position = targetPosition;
				targetMarker.SetActive (true);
				agent.destination = targetMarker.transform.position;
			}
		}
	}
}

Co my tutaj mamy? Pierwszy if sprawdza czy mamy zaznaczoną postać, oraz czy kliknęliśmy klawisz myszki. Jeśli tak, to korzystamy ze screenPointToRay, jak w poprzednim tutorialu, aby określić jakie odzwierciedlenie w świecie, ma kliknięcie na ekranie. Deklarujemy też zmienną RaycastHit do zebrania danych.

Następnie wystrzeliwujemy promień i jeśli w coś trafiliśmy, to sprawdzamy czy nie jest to gracz albo przeciwnik wykorzystując tagi. W innych grach, może być więcej warunków, albo korzystniejsze będzie oznaczenie w jakiś sposób podłoża.

Później tworzymy sobie wektor określający cel gracza. Tutaj dodatkowa informacja. Jeśli chcemy żeby gra rozgrywała się na siatce (czyli kwadraty o stałym boku), możemy linijkę zapisać tak:

Vector3 targetPosition = new Vector3 (Mathf.Round (hitInfo.point.x), Mathf.Round (hitInfo.point.y), Mathf.Round (hitInfo.point.z));

Wtedy sprowadzimy wartości do liczb całkowitych, niezależnie od kliknięcia.

Następne dwie linijki, to ustalenie nowego położenia markera i aktywowanie go. Na koniec do zmiennej destination naszego agenta, przypisujemy pozycje markera. Destination to pozycja w świecie, do której ma dotrzeć dany agent.

Kod jeszcze nie zadziała, ale jesteśmy już blisko. Załatwmy jeszcze chowanie markera. Do funkcji Update dopisujemy to:

if (Vector3.Distance (transform.position, targetMarker.transform.position) < 2.0f) {
	targetMarker.SetActive (false);
}

Sprawdzamy dystans pomiędzy pozycją gracza i markerem. Jeśli jest mniejszy od 2, to chowamy marker przez dezaktywację. Dla Twojego projektu odpowiednia liczba może być inna.

NavMesh

Teraz czas umożliwić ruch, czyli dodać NavMesha. Aby to zrobić, należy uruchomić okienko Navigation. [Window -> Navigation]. Dostaniemy coś takiego:

Okienko Navigation
Okienko Navigation

Nie chce teraz wchodzić w szczegóły konfigurowania NavMesha. Obiecuje, że przy innej okazji to zrobię. Teraz wystarczy nam nacisnąć przycisk Bake, znajdujący się na samym dole. Po tym, powinniśmy zobaczyć na scenie niebieskie obszary:

NavMesh
NavMesh

Niebieski kolor, oznacza obszary, po których postać może się poruszać. I w sumie tyle nam na razie wystarczy wiedzieć. Teraz można już odpalić grę i bawić się sterowaniem postacią.

Bonus: Ograniczony zasięg

Może się zdarzyć tak, że w przypadku chęci ograniczania zasięgu ruchu postaci, np. dla gry turowej, możemy się zdecydować na jakieś punkty ruchu. W zaproponowanym kodzie, można łatwo taką funkcję dodać:

Wystarczy wnętrze ifa z funkcji Update zmienić na coś takiego:

Vector3 targetPosition = new Vector3 (Mathf.Round (hitInfo.point.x), Mathf.Round (hitInfo.point.y), Mathf.Round (hitInfo.point.z));
playerGroundPosition = new Vector3 (transform.position.x, transform.position.y - (mesh.bounds.size.y / 2), transform.position.z);
int moveCost = (int)Mathf.Round (Vector3.Distance (targetPosition, playerGroundPosition));

if (moveCost < curMovePoints && moveCost > 1) {
	curMovePoints -= moveCost;
	targetMarker.transform.position = targetPosition;
	targetMarker.SetActive (true);
	agent.destination = targetMarker.transform.position;
}

Czym są kolejne zaznaczone linijki? Pierwsza linijka oblicza pozycję stóp postaci. Po co nam to? Obrazek wyjaśnia sprawę:

Odległości w zależności od punktu pomiaru
Odległości w zależności od punktu pomiaru

Jeśli liczylibyśmy odległość od środka postaci, a nie od stóp, to mamy zakłamaną faktyczną odległość, co widać na obrazku. Przez to pokonanie np. 3 metrów, byłoby liczone jak pokonanie 4. Im większe odległość, tym błąd byłby większy.

Obliczanie kosztu to zwykła odległość przeliczona na inta. Założyłem tutaj, że pokonanie 1 pola to jeden punkt akcji.

Dodatkowy if, sprawdza czy mamy dość punktów akcji, żeby wykonać ruch. Tutaj zakładamy, że zmienna curMovePoints, to aktualna liczba punktów ruchu danej postaci. Dodatkowo sprawdzamy, czy koszt ruchu jest większy od 1. Przez to, gracz nie straci punktów ruchu, gdyby kliknął w odległości 0.5 od gracza, co oznacza dalej pole na którym stoi, więc ruchu by nie było, a po zaokrągleniu dostaniemy koszt 1 punktu.

Na koniec odejmujemy od punktów akcji koszt ruchu.

Bonus 2: Ciągły ruch postaci

A co w przypadku, gdy chcemy, żeby postać podążała za myszką niczym w Diablo. Tzn. raz klikam klawisz myszy i do póki go trzymam, to postać ma cały czas podążać za kursorem?

Tutaj sprawa jest bardzo prosta, wystarczy ten kod:

if (Input.GetMouseButtonDown (0) && isSelected) {

Zmienić na taki:

if (Input.GetMouseButton (0) && isSelected) {

Teraz do póki postać jest zaznaczona, a klawisz myszy wyciśnięty, znaczniki będą stawiane cały czas, a postać będzie za nimi ciągle podążać. Tutaj warto dla optymalizacji zrezygnować ze stawiania znacznika.

Podsumowanie

W połączeniu z poprzednim tutorialem, oraz z tutorialem na temat kamery, mamy już mocne podwaliny do napisania prostej gry strategicznej, albo sterowanego myszką slashera typu Diablo.