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

Dzisiejszy odcinek: Przeźroczyste ściany między postacią i kamerą

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

Światy gier starają się być jak najbardziej realne. Tym samym w różnych grach, często mamy budynki (wow!). Jednak co w sytuacji, gdy postać wejdzie za ścianę? Tracimy ją z pola widzenia, co raczej nie jest korzystne. Dlatego przez lata, gry wypracowały sobie pewną konwencję, mianowicie, jeśli między postacią i kamerą znajdzie się przeszkoda, to czynimy ją półprzeźroczystą. Tak działo się w Diablo, Jagged Alliance, XCom, Falloucie i masie innych gier.

Przeźroczyste ściany w Diablo
Przeźroczyste ściany w Diablo

Dziś osiągniemy właśnie taki efekt.

Przygotowanie

Ja do stworzenia sceny testowej wykorzystałem klasycznie standardowe assety prototypowania i paczkę Characters. Przygotowana scena wygląda u mnie tak:

Scena testowa
Scena testowa

Mamy tutaj 3 ściany, podłogę, kamerę główną i ThirdPersonController.

Wszystkim elementom otoczenia (podłoga i ściany) dodajemy Box Collider [Component -> Physics -> Box Collider]. Będziemy operować na kolizjach, więc jest to ważne. Domyślnie collider pewnie i tak się pojawi, bo raczej nikt nie chce żeby postać przenikała przez ściany.

Teraz stworzymy sobie nową warstwę (Layer). Aby to zrobić, wybieramy dowolny obiekt i w menu rozwijanym Layer (w inspektorze) wybieramy opcję Add Layer.

Tworzymy nową warstwę
Tworzymy nową warstwę

W okienku które się teraz pojawi w pierwszej wolnej pozycji wpisujemy nazwę nowej warstwy. Ja swoją nazwałem Walls, bo u mnie przeźroczyste będą się robiły ściany, ale możesz warstwę nazwać dowolnie.

Nazywamy warstwę
Nazywamy warstwę

Teraz nowo utworzoną warstwę przypisujemy do wszystkich obiektów, które będą robiły się przeźroczyste. Jak wspominałem, u mnie będą to ściany, więc tym obiektom przypisałem nową warstwę. Robimy to z tego samego menu, gdzie wybieraliśmy “Add Layer”, jednak tym razem klikamy w nazwę naszej warstwy.

Tworzymy skrypt

Czas przygotować skrypt. Ja swój nazwałem Transparent.cs. Skrypt przypisujemy do obiektu kamery.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Transparent : MonoBehaviour {
 // Tu będzie reszta
}

Pierwsza sprawa to dodanie biblioteki Generic. Będziemy wykorzystywać listę, więc bez tej biblioteki nic byśmy nie zrobili.

Teraz dodajmy sobie kilka zmiennych pomocniczych:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Transparent : MonoBehaviour {

	private int layerMask = 1 << 8;
	private List<Transform> hiddenObject;

	public GameObject player;

	// Tu będą się pojawiać kolejne funkcję
}

Pierwsza zmienna to maska warstwy. Brzmi groźnie i wymaga znajomości kodu binarnego.

Maska to nic innego jak liczba rozumiana binarnie, czyli zero-jedynkowo. Gdy zapiszemy sobie np. liczbę 5 binarnie, to otrzymamy: 101. Maska działa tak, że przepuszcza bity gdzie maska ma wartość 1, a blokuje przy 0.

Żeby wyjaśnić sens potrzebujemy drugiej liczby. Np. 3, czyli: 011. Jeśli przepuścimy 3, przez maskę 5 to otrzymamy: 001. Czemu?

Zapiszmy to sobie tak:

3: 011

5: 101

Jeśli będziemy patrzyć na liczby w pionie: Na pierwszej pozycji maska ma wartość 1, więc przepuszcza wartość z liczby, w naszym przypadku 0. Druga pozycja. Trójka ma 1, ale maska ma 0, więc blokuje wartość, zwracając 0. Na ostatniej pozycji, liczba ma wartość 1 i maska również 1, czyli przepuszcza wartość.

Maski są swego rodzaju filtrem. W naszym przypadku maska posłuży do odfiltrowania pozostałych warstw. Czyli, będziemy chcieli szukać tylko elementów na naszej nowej warstwie – później wyjaśnię dlaczego.

Wiemy już czym jest maska i do czego ją wykorzystamy. Ale co znaczy 1 << 8?

Operator “<<” to przesunięcie bitowe. Czyli niemal dosłownie przesuwa bity w liczbie w lewo. Bity przesuwane są w liczbie po lewej stronie operatora o tyle razy ile wskazuje wartość po prawej.

Jedynka bitowo to: 1. Gdy przesuniemy ją 8 razy w lewo dostaniemy: 10000000. Wróćmy na chwilę do obrazka gdzie tworzyliśmy warstwę. Jak widać, mamy 7 wbudowanych warstw i my zaczynamy tworzenie od 8 warstwy. Teraz, gdy wiemy czym jest maska oraz jaką wartość ma nasza, możemy się domyślić co ona zrobi. Jeśli liczbę wszystkich warstw zaprezentujemy tak:

11111111, a maskę tak: 10000000, to widzimy bardzo jasno, że nasza maska przepuszcza tylko pierwszą (a w zasadzie ostatnią, bo liczmy od prawej) warstwę, czyli tą którą my dodaliśmy. Tyle w kwestii masek.

Następna zmienna to lista, gdzie będziemy zbierać ukryte obiekty, a ostatnia zmienna to postać gracza. Można teraz przeskoczyć na chwilę do Unity i uzupełnić zmienną publiczną gracza, przenosząc tam ThirdPersonController.

void Start() {
	hiddenObject = new List<Transform> ();
}

Funkcja start jest prosta. Tylko inicjujemy listę.

Czas na mięsko czyli najważniejszą funkcję.

void Update () {

	Vector3 direction = player.transform.position - transform.position;

	RaycastHit[] hits = Physics.RaycastAll(transform.position, direction, Mathf.Infinity, layerMask);
	Debug.DrawRay (transform.position, direction, Color.red);

	// Tutaj będą dwa fory!

}

Funkcja składa się z dwóch pętli, które za chwilę omówię oddzielnie. Zaczniemy jednak od zmiennych:

Jak pewnie łatwo się domyślić, określenie jaki obiekt trzeba uczynić przeźroczystym, głównie sprowadza się do odkrycia, które obiekty znajdują się na drodze z kamery do postaci. My do tego posłużymy się rzucaniem promienia (RayCasting).

Żeby móc rzucić promień, musimy mieć punkt zaczepienia i kierunek. Punkt zaczepienia to oczywiście kamera (do której skrypt jest przypisany). Do wyznaczenia kierunku służy pierwsza zmienna (direction). Posługujemy się tu najprostszą wiedzą z geometrii. Gdy mamy 2 punkty, to odejmując współrzędne początku (A), od współrzędnych końca (B), dostajemy wektor, określający kierunek z punktu A do B.

Kolejna linia to już samo RaycastAll. Różni się tym od klasycznego Raycast, że nie zatrzymuje się na pierwszym trafionym obiekcie, tylko zwraca wszystko na co trafi. Parametry to kolejno: Pozycja kamery, obliczony wcześniej kierunek, jako długość rzucanego promienia określiłem tutaj nieskończoność, ale gdy mamy już konkretną grę można to ograniczyć. Ostatni parametr to nasza maska.

Maskę dodałem z prostego powodu. Sprawdzamy tylko obiekty, które są na wybranej warstwie. Dzięki temu, np. podłoga nie stanie się przeźroczysta (Jeśli nie dodamy jej do warstwy). Można by to sprawdzać ręcznie, tylko po co?
[stextbox id=”info” defcaption=”true”]Tutaj można przeprowadzić test i na chwilę usunąć parametr maski i zobaczyć jak to wpłynie na całość kodu.[/stextbox]
Debug.DrawRay, rysuje nam promień – dodałem aby ułatwić debugowanie.

Czas na pierwszą pętle:

for (int i = 0; i < hits.Length; i++) {
	RaycastHit hit = hits[i];
	Transform currentHit = hit.transform;

	if (!hiddenObject.Contains (currentHit)) {
		hiddenObject.Add (currentHit);
		Renderer rend = hit.transform.GetComponent<Renderer>();

		if (rend) {
			rend.material.shader = Shader.Find("Transparent/Diffuse");
			Color tempColor = rend.material.color;
			tempColor.a = 0.3F;
			rend.material.color = tempColor;
		}
	}
}

Lecimy sobie forem po wszystkich trafionych, przez rzucony promień obiektach.

Najpierw do zmiennej hit wrzucamy informację o obecnie przerabianym trafieniu, a do zmiennej currentHit informację o komponencie transform tego obiektu. Nie trzeba tego robić, ale jest przejrzyściej, niż stosować wszędzie hits[i] i hits[i].transform.

Pierwszy if, sprawdza czy dany obiekt znajduje się w liście ukrytych obiektów – jeśli już tam jest, to nie musimy go znów dodawać.

Jeśli go nie ma, to zaczynamy od dodania go, a później pobieramy jego renderer. Jeśli obiekt posiada renderer, to przystępujemy do schowania go (gdyby nie miał renderera, to jest niewidoczny, więc nie musimy nic z nim robić).

Czas na samą zmianę obiektu w półprzeźroczysty. Pierwszy krok to zmiana shadera na Transparent/Diffuse. Ten shader obsługuje przeźroczystość (można użyć innego, ale również musi obsługiwać przeźroczystość). Następnie tworzymy kolor tymczasowy, który opieramy na kolorze faktycznym obiektu, zmieniamy wartość kanału alfa na jakiś ułamek i przypisujemy nowy kolor do materiału. Dzięki temu, że skorzystaliśmy z faktycznego koloru, zasadniczo zmieniliśmy tylko przeźroczystość, więc nie tracimy informacji o obiekcie.

No dobra, ale jak już coś zniknęliśmy, to trzeba to “odzniknąć”, gdy gracz wyjdzie zza tego obiektu.

for (int i = 0; i < hiddenObject.Count; i++) {

	bool isHit = false; 
	for (int j = 0; j < hits.Length; j++) {
		if (hiddenObject [i] == hits [j].transform) {
			isHit = true; 
			break;
		}
	}

	if (!isHit) {
		Renderer rend = hiddenObject[i].transform.GetComponent<Renderer>();
		rend.material.shader = Shader.Find("Standard (Specular setup)");
		hiddenObject.RemoveAt(i);
	}
}

Druga pętla śmiga sobie po tablicy ukrytych obiektów.

Pierwsza sprawa, to wykrycie czy dany element tablicy dalej jest na drodze promienia. W tym celu porównujemy tablicę ukrytych obiektów z tablicą obecnie trafionych obiektów. Jeśli obiekty się pokrywają to ustawiamy flagę na true i wykonujemy break, dzięki temu wewnętrzna pętla jest przerywana i nie musimy śmigać całej tablicy tracąc czas i zasoby.

W przypadku gdy przelecimy całą pętle i obiektu nie znajdziemy, oznacza to, że obiekt już nie powinien być przeźroczysty. Wtedy do akcji wkracza if.

Ponownie pobieramy renderer, jednak teraz tylko zmieniamy shader. Można tutaj skorzystać z dowolnego shadera – oczywiście najlepiej ustawić ten, który domyślnie obiekt posiadał. Ja ustawiłem domyślny shader dla obiektów z paczki Prototyping. Na koniec usuwamy obiekt z listy ukrytych obiektów.
[stextbox id=”info” defcaption=”true”]Uważna osoba zauważy, że nie zmienialiśmy tutaj kanału alfa dla koloru. Dzieje się tak, ponieważ wybrany shader nie obsługuje przeźroczystości, dzięki temu ustawiony kanał alfa nie ma wpływu na nic. Jeśli Twój domyślny shader będzie obsługiwał przeźroczystość, ustawienie pierwotnej wartości kanału alfa może być konieczne.[/stextbox]
W sumie tyle. Efekt końcowy prezentuje się tak:

Efekt końcowy
Efekt końcowy

Podsumowanie

Tym sposobem osiągnęliśmy prosty system do pokazywania naszej postaci, nawet gdy jest zasłonięta. Efektem przeźroczystości można się bawić stosując różne inne shadery i bajery.

Ktoś może spytać co w przypadku posiadania wielu postaci? Zwyczajnie umieszczamy je w tablicy i sprawdzamy te wartości dla każdej z nich (w Update pojawi się for ogarniający wszystko i śmigający po tej tablicy postaci).

Podoba Ci się? Udostępnij!