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

Dzisiejszy odcinek: Opis przedmiotu nad obiektem

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

W większości gier musimy podnosić przedmioty, albo nawiązywać interakcję z postaciami. W grach mamy dwa główne sposoby do pokazania graczowi, że może nawiązać z czymś interakcję, a czasami mu powiedzieć czym to coś jest. Bardzo klasyczne jest połyskiwanie przedmiotu, który da się podnieść. Czasami ogranicza się to do ramki (ale nie o tym dzisiaj). Drugą, chyba nawet starszą metodą jest wyświetlenie opisu nad przedmiotem czy postacią. Takie coś możemy spotkać w Gothicu czy Wiedźminie:

GUI sugeruje, że z postacią można porozmawiać.
GUI sugeruje, że z postacią można porozmawiać.

Przygotowanie

Jedyne co będziemy potrzebować do testów, to kontroler postaci ze standardowych assetów i kilka luźnych Cubów. Zaczniemy od zaimportowania Assetów. Aby to zrobić wybieramy: [Assets -> Import Package -> Characters] i importujemy wszystko.

Import paczki Characters ze standardowych assetów
GIF: Import paczki Characters ze standardowych assetów

Aby było nam nieco łatwiej, importujemy również assety prototypowania: [Assets -> Import Package -> Prototyping] – Nie dodaje GIFa, bo robi się to tak samo jak w przypadku postaci.

Teraz przygotujemy sobie scenę testową. Dodajemy sobie na środek podłogę z pakietu Prototyping, FPSController z pakietu Characters oraz 3 różne figury ze standardowych obiektów 3D [GameObject -> Create 3D -> Cube/Sphere/Capsule]. Na koniec usuwamy też obiekt Main Camera, bo nie potrzebujemy 2 kamer na scenie.

Przygotowanie sceny
GIF: Przygotowanie sceny

Namierzamy obiekt

Teraz został nam etap kodowania. Tworzymy sobie nowy skrypt (ja swój nazwałem Labeller.cs) – klikamy prawym klawiszem na wolnym polu okienka Project i wybieramy: [Create -> C# Script] i przypisujemy go od razu do obiektu FirstPersonCharacter, który jest potomkiem (childem) obiektuFPSController. Skrypt najlepiej umieścić swobodnie w projekcie. Tzn. nie umieszczać go w folderze Standard Assets! Wchodzimy w edycję skryptu, klikając na niego dwukrotnie.

Pierwszy etap naszego kodownaia, daje nam coś takiego:

using UnityEngine;
using System.Collections;

public class Labeller : MonoBehaviour
{
    private RaycastHit hit;
    private Vector3 fwd;
    private int range = 3;

    void Update()
    {
        fwd = transform.TransformDirection(Vector3.forward);

        if (Physics.Raycast(transform.position, fwd, out hit)) {
            if (hit.transform.tag == "Item" && hit.distance < range) {
                Debug.Log("Cos mamy!");
            }
        }
    }

Bardziej obeznany użytkownik zauważy, że jest to nic innego jak rzucanie promienia, znane nam z wszelkich tutoriali dotyczących strzelania. Analizując kod pokolei:

private RaycastHit hit;
private Vector3 fwd;
private int range = 3;

Tutaj mamy tylko deklaracje zmiennych. Pierwsza to zmienna zbierające dane o trafieniach rzucanego promienia, druga to wektor wskazujący przód, a ostatnia to zasięg, do którego chcemy rzucać promień. Czas na kod z funkcji update:

fwd = transform.TransformDirection(Vector3.forward);

Tym kawałkiem kodu określamy gdzie jest przód naszej postaci. Bez tego, moglibyśmy rzutować promień np. z pleców, co byłoby bez sensu.

if (Physics.Raycast(transform.position, fwd, out hit)) {
    if (hit.transform.tag == "Item" && hit.distance < range) {
        Debug.Log("Cos mamy!");
    }
}

Pierwsza linijka to rzucanie promienia. Pierwszy parametr to skąd, drugi w jakim kierunku (dlatego określaliśmy przód), a ostatni to nasz zmienna zbierająca dane o obiekcie na który trafiliśmy. Słówko kluczowe out, wyrzuca nam dane z funkcji. Jest to tak zwana referencja. Czyli nie podajemy zmiennej bezpośrednio do funkcji, a podajemy jedynie adres w pamięci, który zmienna zajmuje. Dzięki temu modyfikujemy obszar pamięci. Więc gdy funkcja się skończy wykonywać, w naszej zmiennej hit, będą dane, które wprowadziły tam operacje wewnątrz funkcji. Skracając: zmienna hit, przetrzymuje dane o trafionym obiekcie. Cały kod wstawiamy w ifa, dzięki temu kod wewnątrz ifa, wykona się tylko wtedy gdy otrzymamy trafienie.

Kolejny if, sprawdza czy obiekt w który trafiliśmy ma tag Item oraz czy jest w zasięgu jaki sobie ustaliliśmy. Jeśli tak, to wypisujemy sobie coś w konsoli.

Zmienna range dobrana była eksperymentalnie. U Ciebie inna wartość może być odpowiednia. Trzeba zwyczajnie popróbować.

Jest jeden problem. Póki co, nic nie namierzymy, bo żaden z obiektów, nie ma tagu “Item”. Musimy to zmienić. Wybieramy jeden z obiektów (Cube/Sphere/Capsule). W panelu Inspector klikamy w tag i wybieramy opcję Add Tag. W nowym oknie na liście tag, klikamy plusa i wpisujemy w inputa “Item”. Wracamy do naszych obiektów i wybieramy im nowo utworzony tag.

Dodanie Tagów
GIF: Dodanie Tagów

Teraz można już przetestować ten fragment kodu.

Wyświetlamy tekst

Czas lekko zmodyfikować kod, aby wyświetlić jakiś tekst:

using UnityEngine;
using System.Collections;

public class Labeller : MonoBehaviour
{
    private RaycastHit hit;
    private Vector3 fwd;
    private int range = 3;
    private Camera camera;
    private Vector3 screenPos;
    private bool labelDraw = false;
    private string labelText;

    void Start()
    {
        camera = gameObject.GetComponent<Camera>();
    }

    void Update()
    {
        fwd = transform.TransformDirection(Vector3.forward);
        labelDraw = false;

        if (Physics.Raycast(transform.position, fwd, out hit)) {
            if (hit.transform.tag == "Item" && hit.distance < range) {
                labelDraw = true;
                screenPos = camera.WorldToScreenPoint(hit.transform.position);
                labelText = hit.collider.gameObject.name;
            } 
        }
    }

    void OnGUI()
    {
        if (labelDraw) {
            GUI.Label(new Rect(screenPos.x, Screen.height - screenPos.y - ( (1 / screenPos.z) * 500), 100, 20), labelText);
        }
    }
}

Oto nowa postać kodu. Wszystkie zmiany zaznaczyłem, a teraz je sobie po kolei omówimy:

private Camera camera;
private Vector3 screenPos;
private bool labelDraw = false;
private string labelText;

Pomocnicze zmienne. Pierwszy przechowuje obiekt kamery, żeby się do niego nie odwoływać co chwilę. Drugi określa gdzie wyświetlić tekst, kolejna zmienna mówi, czy mamy tekst do wyświetlenia, a ostatnia określa tekst.

void Start()
{
    camera = gameObject.GetComponent<Camera>();
}

Komponent kamery znajduje się bezpośrednio w obiekcie, do którego należy skrypt, dlatego możemy się do niego tak łatwo odwołać. Robimy to w funkcji Start, aby zrobić to tylko jeden raz i nie szukać kamery każdorazowo (oszczędzamy zasoby).

labelDraw = false;

Domyślnie ustawiamy flagę określającą czy na coś trafiliśmy na false.

labelDraw = true;
screenPos = camera.WorldToScreenPoint(hit.transform.position);
labelText = hit.collider.gameObject.name;

Wewnątrz ifów, w momencie kiedy trafiliśmy na obiekt spełniający wymagania podejmujemy następujące kroki: Ustawiamy flagę na true, aby poinformować resztę skryptu, że na coś trafiliśmy. Następnie określamy gdzie mamy wyświetlić tekst. WorldToScreenPoint to sprytna funkcja, która przerabia nam pozycję jakiegoś punktu w przestrzeni świata gry, na dwuwymiarową przestrzeń ekranu gracza. hit.transform.postion to pozycja obiektu, w który trafiliśmy rzutowanym promieniem. Ostatnia linijka to przypisanie do naszej zmiennej stringowej nazwy obiektu. Musimy się odwołać do niej w taki śmieszny sposób, bo w przeciwnym wypadku… no cóż… kod nie zadziała.

void OnGUI()
{
    if (labelDraw) {
        GUI.Label(new Rect(screenPos.x, Screen.height - screenPos.y - ( (1 / screenPos.z) * 500), 100, 20), labelText);
    }
}

Na koniec funkcja OnGUI, odpowiedzialna za wyświetlanie GUI. Prosty If, który sprawdza czy jest coś do wyświetlenia, a we wnętrzu GUI.Label czyli wyświetlenie tekstu. Pierwszy parametr to obiekt typu Rect, który mówi w jakiej pozycji i o jakiej rozmiarze labela będziemy wyświetlać. Drugi parametr to nasz string, który wyświetlamy.

Zostało wyjaśnić parametry w obiekcie Rect. Zacznę od tyłu: 100, 20 to rozmiar labela, u mnie 100px na 20px. W tym zadaniu nie miało to większego znaczenia, więc wrzuciłem cokolwiek, aby tylko tekst się wyświetlił.

Ważniejsze są pierwsze dwa parametry odpowiedzialne za pozycję. Pierwszy jest prosty i jest to współrzędna X naszego obiektu. Gorzej jest z drugim parametrem, bo proste wstawienie współrzędnej Y sprawiało, że tekst latał w górę i w dół. Dlatego trzeba było wprowadzić pewne parametry:

Screen.height – screenPos.y – Najpierw zamiast wprowadzać współrzędną, odjąłem ją od maksymalnej wysokości ekranu. Dzięki temu tekst pojawiał się w środku obiektu. Ale zawsze stabilnie.

Należało go podnieść o pewną stałą wartość. Wartość uzależniłem od odległości gracza od obiektu (zapewniała nam to współrzędna z). Ale skąd aż takie rozbudowanie: ( (1 / screenPos.z) * 500)? Ponieważ współrzędna Z posiadała wartości między 0.5 do 2.5 musiałem pomnożyć je przez dużą wartość, aby dostać zauważalne efekty. Liczba 500 sprawdziła się tu perfekcyjnie. Jednak był drugi problem. Tekst powinien być tym wyżej im bliżej jesteśmy, ale współrzędna z malała im byliśmy bliżej obiektu. Dlatego, korzystamy z jej odwrotności (1 / z), dzięki temu im bliżej jesteśmy, tym mnożnik jest większy. Mam nadzieję, że jest to w miarę zrozumiałe.

Mnożnik 500 (czyli offset) może być różny w zależności od obiektów jakich używasz w grze, więc możliwe, że będzie tu trzeba nieco pokombinować i popróbować.

Podsumowanie

W sumie tyle. Pojawia nam się piękny tekst, będący statyczny nad obiektem. Można się jeszcze bawić w ustawianie jego wyglądu i inne bajery, ale najważniejsza część, jest tutaj zrobiona.