Player menu. Log #12 (Unity, C#)
22.01.2018

Jako každé RPG tak i to mé bude mít inventář, atributy a další informace o postavě. K tomu je však potřeba nějak přistupovat, obvykle po stisknutí klávesy I nebo TAB se zobrazí inventář nebo jiné okno kde je přístup k předmětům hráče a jeho statistikám. Já však budu mít inventář a atributy hráče odděleně. Představuji si mít malé menu ve tvaru prstence, které se objeví kolem hráče a jednotlivé tlačítka budou výseče toho prstence.

Základem UI v Unity je Canvas, bez něj nejde UI tvořit. Do scény vložím tedy Canvas (UI>Canvas) a přejmenuji ho na UI. Mám tedy vytvořený kořenový objekt, resp container pro většinu mých uživatelských rozhraní. Pro udržení pořádku ve scéně a v mém UI, bude mé menu jako samostatný panel. Kliknu tedy pravým a dám UI>Panel, ten pojmenuji PlayerMenu. Stěžejní komponentou každého UI prvku je RectTransform. Dvě nedílné součásti a hlavní funkční prvky jsou anchors a pivot.

Anchors definují vztažné rohy a okraje RectTransform. Jsou to kotvy (bílé trojuhélníčky kolem rohů panelu), které udávají "chování" prvku. Tyhle kotvy mají minimum a maximum na osách X a Y. Ty jsou v hodnotách od 0 do 1, kde se minimum na ose X bere 0 jako levá hrana nadřazeného prvku a na ose Y je 0 spodní hrana nadřazeného prvku, 1 je pak pravá horní. Jak už ze slova anchors - kotvy vyplývá, jedná se o fixní body, které jsou závislé na nadřazeném - rodičovském prvku, v mém případě UI Canvasu, ten zase na velikosti, resp. rozlišení obrazovnky. Pozice kotev se vůči nadřazenému prvku nemění. Pokud se bude tedy rozlišení měnit, bude se měnit i velikost canvasu a tedy i velikost mého panelu. Uvedu příklad. Jestliže chci panel, který bude vždy na středu a bude měnit svoji velikost spolu s nadřazeným prvekm, nastavím minimum i maximum na ose X na 0.5, osu Y ponechám 0 min a 1 max. Na pozici anchors je taky závislé zadávání velikosti panelu. V defaultním nastavení, kdy panel kopíruje rodičovský prvek, se zadáva padding - vzdálenost hran panelu od anchors hran jako Left, Right, Top a Bottom. V případě, kdy je hodnota minima i maxima stejná v jedné z os X nebo Y, stává se zadávání v téhle ose absolutní vůči pozici hrany anhoru. V mém uvedeném příkladě kdy je na ose X minimum i maximum 0.5, místo Left a Right paddingu, zadávám Pos X a Width, tedy šířku. Ty jsou pak závislé na pivotu.

Pivot (modrý prstenec) udává nulový vztažný bod pro velikost a pozici. V defaultním případě nehraje pivot příliš roli pro padding (pro některé Layout komponenty může ale být důležitý, rozvedu později). Opět uvedu na mém příkladu. Nyní je hodnota pivotu nastavena na 0.5 na ose X, to znamená, že pivot je na stejné pozici jako hrany anchors pro osu X. Od této pozice se bude počítat Pos X i velikost. Pokud nastavím pivot na ose X na 0, tedy na levou hranu mého panelu, aktuální Pos X se nastaví automaticky na zápornou hodnotu poloviny sířky (nutno si uvědomit že Unity automaticky nehýbe s objektem, ale pouze přizpůsobí jeho hodnoty pozice), pokud změním Pos X na 0, celý prvek se přesune doprava. Doprava proto, že pivot udává počáteční bod panelu, když tedy nastavím Pos X nastavuji vlastně pozici pivotu, od kterého se pak odvíjí šířka. Je li pivot vlevo, šířka poroste doprava, a naopak.

V případě více panelů, je možné využít anchors k poměru velikostí nadřazeného prvku. Chtěl bych li mít jeden panel na 40% velikosti rodiče a druhý aby vyplňoval zbytek. Nastavím prvnímu prvku na ose X minimum na 0 a maximum na 0.4, druhému pak minimum na 0.4 a max na 1. Oba prvky pak budou dodržovat poměr. 

Nastavování anchors a pivotu lze provést i ryhleji. Vlevo od paddingu či velikostí a pozic je okýnko, kde se dají nastavit různé možnosti. Pomocí shfitu pak nastavuji i pivot a s alt i pozici.

Moje player menu bude mít ale malinko odlišné chování, protože já budu dynamicky měnit jeho lokální poizici na základě pozice postavy, není nutné se starat o anchors. Nastavím všechny 4 hodnoty na 0 a jeho velikost prozatím ponechám. Změním jen alpha kanál komponenty Image na 0, ať je panel průhledný. Základem mého menu bude tlačítko, tvořící jednu výseč prstence, tento tvar jsem si připravil v grafickém editoru a uložil jako png do assets UI>UI Textures. Pro účely UI je nutné nastavit Texture Type na "Sprite (2D and UI)". Vložím pod můj panel PlayerMenu nové tlačítko (UI>Button), přejmenuji ho na Iventory_button a do komponenty Image jako Source Image vložím můj připravený tvar, změním barvu na odstín žluté, jelikož bude působit pouze jako rámeček a zaškrtnu Preserve Aspect, abych zachoval poměr stran obrázku. Nastavím velikost na 370x210. Místo podřazeného objektu Text, vložím nový obrázek (UI>Image) přejmenuji ho na Background a požiju na něj stejný tvar, jen nastavím padding na 2px pro všechny strany, změním barvu na tmavě šedou a odšrtnu políčko RaycastTarget, aby objekt neblokoval kliknutí nebo najetí myši. Vložím ještě jeden obrázek jako ikonu. Opět tedy vytvořím image a tentokrát nastavím velikost na 120x120, přetáhnu do něj ikonu batohu, kterou jsem stáhl na game-icons.net. Ikonu nechám na středu tlačítka, jen nastavím Pos Y na 10, a pozadí nechám roztáhlé podle tlačítka (strech) pouze s tím paddingem a nastavím černou barvu. Tlačítko pak umístím na levý střed panelu. Stejným způsobem vytvořím další dvě tlačítka, jen vrchní nastavím na horní střed panelu a pravé na pravý střed. Celý panel zmenšuji dokud nedosáhnu požadovaného vzhledu, tedy aby menu vypadalo jako prstenec, bez spodní části. Celé menu pak vypadá takhle.

 

Player Menu

 

Já ovšem nechci aby menu bylo stále na obrazoce, ale aby se objevilo jen pokud držím klávesu TAB. Vytvořím tedy script PlayerUIMenuManager a přetáhnu jej na panel PlayerMenu. Menu bude jediné ve hře, hodí se mi tedy udělat singelton z této třídy, využiju to hlavně u přístupu k této třídě, jelikož nemusím vytvářet její instanci v každé tříde, ale přistupuji pouze k jediné instanci kterou si deklaruji přímo v této tříde. Pak potřebuji znát pozici kde chci aby se mi menu objevilo. K tomu vytvořím veřejnou proměnnou GameObject headPosition do které v inspektoru vložím objekt na jehož pozici chci menu. Ten v hierarchii postavy vytvořím jako EmptyObject a přejmenuji jej na HeadPivot a posunun kam potřebuji, poté přetáhnu do inspektoru ve scriptu PlayerUIMenuManager. Ted vytvořím funkci Repos() která bude nastavovat pozici menu na pozici HeadPivot. Tady je potřeba si uvědomit, že UI pracuje ve 2D ale HeadPivot má souřadnice ve 3D prostoru. Opět je tedy potřeba převést prostorovou souřadnici na 2D souřadnici obrazovky, k tomu slouží funkce hlavní kamery WorldToScreenPoint(), která jako parametr bere Vector3 souřadnice. Teď jsou dvě možnosti jak nastavovat pozici v kódu, buď měnit localPosition RectTransform nebo anchoredPosition. Rozdíl je v tom, že anchoredPosition je pozice pivotu vůči anchors, dá se tedy říct že absolutní poloha panelu, zatímco localPosition je pozice relativní v závislosti na nadřazeném prvku. anchoredPosition nebylo úplně ideální, protože menu nebylo horizontálně na středu z nějakého důvodu, ale při nastavení localPosition, už to bylo v pořádku, jen je potřeba navíc od výškové souřadnice HeadPivotu odečíst Y souřadnici pozice nadřazeného prvku. Aby menu nebylo vidět, přesouvám ho vždy po spuštění klávesy TAB mimo obrazovku - mimo canvas, pomocí funkce MoveOut(). Nastavuji tedy opět localPosition, jen do Y souřadnice vkládám záporný dvojnásobek Y pozice nadřazeného prvku. Další věcí, kterou chci je, aby mi menu plynule naběhlo a taky plynule zmizelo. K tomu využiji C# Coroutine. Velmi jednoduše řečeno se jedná o funkci jejíž průbeh lze (např.) pomocí funkce WaitForSeconds() pauznout a pak zase pokračovat v činnosti v dalším snímku. Deklaruje se pomocí IEnumerator jakožto návratového typu funkce. Pro náběh tedy vytvořím funkci FadeIn() a pro opak FadeOut(). Ve funkci Repos() hned jako první pustím FadeIn() pomocí StartCoroutine("FadeIn")  a pak nastavuji pozici jak jsem psal výše. Pro zmizení menu první pustím ve funkci MoveOut() koprogram FadeOut() a na konci koprogramu přesunu menu mimo canvas. Pro to aby mohlo menu plynule naběhnout, je potřeba přidat layout komponentu CanvasGroup mému menu, tam je proměnná alpha, této proměnné v kódu budu postupně měnit hodnotu z 0 na 1 po krocích ve for a naopak. To je vše. Kód pak vypadá takhle.

 

public class PlayerUIMenuManager : MonoBehaviour
{

    public static PlayerUIMenuManager Instance { get; private set; }
    public GameObject headPosition;

    // Awake for Singelton
    void Awake()
    {
        if (Instance == null)
            //if not, set instance to this
            Instance = this;
        //If instance already exists and it's not this:
        else if (Instance != this)
            //Then destroy this. This enforces our singleton pattern, meaning there can only ever be one instance of a GameManager.
            Destroy(gameObject);
    }

    public void Repos()
    {
        StartCoroutine("FadeIn");
        Vector3 pos = Camera.main.WorldToScreenPoint(headPosition.transform.position);
        GetComponent<RectTransform>().localPosition = new Vector3(0, pos.y - transform.parent.GetComponent<RectTransform>().localPosition.y, 0);
    }

    IEnumerator FadeIn()
    {
        for (float i = 0; i <= 1.2; i += 0.1f)
        {
            GetComponent<CanvasGroup>().alpha = i;
            yield return new WaitForSecondsRealtime(0.01f);
        }
    }

    IEnumerator FadeOut()
    {
        for (float i = 1; i >= -0.08; i -= 0.1f)
        {
            GetComponent<CanvasGroup>().alpha = i;
            yield return new WaitForSeconds(0.01f);
        }
        GetComponent<RectTransform>().localPosition = new Vector3(0, -transform.parent.GetComponent<RectTransform>().localPosition.y * 2, 0);
    }

    public void MoveOut()
    {
        StartCoroutine("FadeOut");
    }
}

 

Teď zbývá ještě nastavit reakci na klávesu TAB. Pro tuhle funkčnost vytvořím nový script PlayerUIMenu a script přetáhnu na canvas UI. Uvnitř třídy nechám pouze funkci Update() a do ní dvě podmínky. Jednu pro držení klávesy Tab a druhou pro její puštění. Do první podmínky dám volání funkce Repos() na singletona PlayerUIMenuManager.Instance, do druhé volání funkce MoveOut(). Pro klávesu tab nepoužiju klasicky Key.Tab, ale definuji si v Edit>Project Settings>Input novou klávesu kterou pojmenuji jako "Player UI Menu", tu využiji ve funkci GetButton() a GetButtonUp().

 

public class PlayerUIMenu : MonoBehaviour
{
    void Update()
    {
        if (Input.GetButton("Player UI Menu"))
        {
            PlayerUIMenuManager.Instance.Repos();
        }
        if(Input.GetButtonUp("Player UI Menu"))
        {
            if (PlayerUIMenuManager.Instance != null)
            {
                PlayerUIMenuManager.Instance.MoveOut();
            }
        }
    }
}

 

Menu se mi tedy zobrazuje. Teď chci aby se mi nějak zvýrazňovali tlačítka když na ně najedu myší. Zvýrazní se tím, že povyjedou o kousek ven. Toho docílím tak, že na mých jednotlivých tlačítkách v komponentách Button zvolím Transition a dám "Animation". Dole se mi vytvoří tlačítko Auto Generate Animation na něj kliknu, pojmenuju kontroller animace a uložím do assets (UI>Animations). Animaci vytvořím jednoduše tak, že si zobrazím okni Animation (Window>Animation), kliknu na tlačítko které chci animovat a vyberu stav - Highlighted, kliknu na červené kolečko - nahrávání klíčových snímků. Tlačítko (Inventory_button) pak po ose X posunu o 7px doleva (45). Vyberu ikonu a změním jí barvu na odstín žluté - jako rámeček. Vypnu nahrávání snímků a vrátím se ke stavu Normal, tam vrátím vše do původní polohy a stejně tak provedu pro ostatní tlačítka, jen změna pozice bude samozřejmě odpovídat poloze tlačítka.

Ještě chci aby mi menu zobrazovalo text tlačítka na které najedu myší. Vložím tedy pod panel UI objekt Text (UI>Text). Ten přejmenuji na PlayerMenu_Text, velikost 160x30, Y pozici tak aby to vyhovovalo. Abych text měnil každému tlačítku přidám komponentu EventTrigger do komponenty přidám dva eventy, jeden pro Pointer Enter a druhý Pointer Exit, přetáhnu text do políčka pro cílový objekt eventu, pak v menu najdu Text>string text a do textového pole napíšu text který chci aby se zobrazil. Stejné provedu pro ostatní tlačitka. Menu je hotové.