Animace, klipy a pohyb. Log #5 (Unity, C#)
14.03.2017

                Jak bude fungovat pohyb ve hře? Docela jednoduše, když kliknu na místo, kam chci aby mi postava běžela, nastavím navmesh agentovi cíl a spustím animaci postavy. Jakmile dosáhne postava cíle, agent se zastaví a stejně tak i animace běhu. Kdykoliv mezitím bude postava v režimu idle. Jak si ale pamatujete, u postavy mám pouze jednu animaci z blenderu, která obsahuje všechny pohyby. Animace v Unity totiž jde nastříhat na jednotlivé klipy.

                Když vyberu v assets můj objekt postavy, tak v inspektoru je záložka Animations. V něm je pole Clips, kde by měly být dva klipy "Default Take" a "rig|rigAction". Oba začínající na snímku 0, konče snímkem 227. V políčku Clips je dole malé plus, při vybraném "Default Take" na něj kliknu. Pod tím je pole, kde se dá klip pojmenovat a pod tímhle polem je časová osa,  pod ní pak startovní snímek a konečný. Tohle nastavím přesně jak potřebuji podle jednotlivých animací. U klipů, které se mají opakovat, je pak potřeba zaškrtnout políčka Loop time, Loop pose. Po dokončení úprav se musí úplně dole kliknout na tlačítko Apply.


Clips


               Jako další krok rozhýbeme agenta. Na to vytvořím skript PlayerController. V assets vytvořím složku "Scripts>Entities>Player". Uvnitř složky kliknu pravým Create>C# Script. A pojmenuji ho PlayerController. Ten otevřu v jakémkoliv editoru. Unity poskytuje 30denní trial verzi Visual Studia, nebo Mono Develop. Já používám Visual Studio, pokud jste studenti informatiky, je možné že vaše škola poskytuje bezplatně licence na Visual Studio. 

                Není na škodu dobře si promyslet jak bude pohyb fungovat, usnadňuje to programování. Nejsem nijak výborný programátor a tak se omlouvám za kopance, můžete mě na ně upozornit v komentářích. Proto taky ani nebudu nijak extra rozepisovat základní věci a poučovat, předpokládám že programovat umíte.

                Základem pro click to move, je pozice myši kam kliknu. Tohle je ale komplikovanější než se může zdát. Je totiž nutné převést souřadnice myši na souřadnice bodu objektu na který klikám. Zkráceně převod souřadnice z viewportu na lokalní pozici v 3D. Unity má naštěstí funkci zvanou ScreenPointToRay, která vrací typ Ray, to je přímka od souřadnice myši na obrazovce k bodu objektu ve 3D. Jak popis napovídá, je potřeba vytvořit objekt typu Ray do nějž vložím návratovou hodnotu funkce ScreenPointToRay, a do ní vložit parametr souřadnice myši.

                Stále ale nevím souřadnice bodu, kam jsem klikl. K tomu mi pomůže Unity funkce Physics.Raycast. Která vezme můj Ray paprsek a vrhne jej skrz všechny objekty (i collidery) ve scéně, v závislosti na parametru délky paprsku. Užitečný parametr ve funkci Physics.Raycast je RaycastHit, který poskytuje informace o zasaženém objektu. Jedním z nich je i proměnná point, což je souřadnice, kde paprsek prochází. Takže při kliknutím levého tlačítka myši dostanu souřadnice místa, kam chci aby postava šla.

 

ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Input.GetMouseButton(0))
{
	if (Physics.Raycast(ray, out rHit, 100f))
	{
		// Move to position
	}
}


               Teď když mám souřadnice, můžu tam poslat agenta. NavMesh Agent má proměnnou destination do které jen přiřadím souřadnice. Pak jen zavolám Resume() na agenta a ten se dá do pohybu.

 

nma.destination = rHit.point;
nma.Resume();


                Jakmile agent dorazí na místo, je potřeba jej zastavit. To naneštěstí není úplně automatické a už vůbec ne jednoduché. Pokud kliknu do prázdného prostoru, tak zastaví relativně bez problémů, ale jakmile kliknu na objekt, například truhlu, tak prakticky agent nikdy nedorazí na místo cíle a je pořád v pohybu. To je hlavně nutné, když chci zastavit animaci. Agent totiž nemá žádnou funkci která by správně vracela stav, jestli je v cíli či na cestě. Lépe řečeno má hned několik proměnných které by se o to měli starat, ale ty nefungují jak by měli (pathPending, pathStatus). Proto si musím vzdálenost počítat sám. Agent má sice proměnnou stoppingDistance, ale v případě, že klikám do prázdného prostoru terénu, chci aby tato vzdálenost byla téměř nulová. Řeším to tedy tak, že každý 3D objekt ve hře bude mít svůj rádius, který bude udávat nejmenší vzdálenost, na které může hráč zastavit. Pokud tedy kliknu na objekt, získám od něj vzdálenost a tu nastavím agentovi. Pří výpočtu vzdálenosti s tímhle radiusem musím počítat. Další faktor který mi ovlivňuje vzdálenost zastavení je Base Offset agenta. Origin NavMesh agenta je totiž v polovině výšky válce, který jej definuje. To znamená že jeho souřadnice je až nad zemí a nikdy by se nepotkala s bodem cíle. Tohle číslo ovšem také není úplně přesné. Pomocí konzole jsem zjistil skutečnou vzdálenost agenta od cíle a číslo bylo zhruba 1,85 násobek offsetu. Pak už jen rozlišuji jestli jsem klikl na objekt nebo na terén a podle toho nastavuji vzdálenost zastavení. Že jsem klikl na terén poznám, protože jsem nastavil v inspektoru terénu tag "Ground". Cokoliv není tímto tagem označené je považováno za objekt. Celý kód pak vypadá následovně:

 

void Update()
    {
        // Movement
        ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Input.GetMouseButton(0))
        {
            if (Physics.Raycast(ray, out rHit, 100f))
            {
                if (rHit.transform.tag != "Player")
                    MoveToPosition();
            }
        }
        
        destinationDistance = Vector3.Distance(transform.position, rHit.point) - (nma.baseOffset * 1.85f) - nma.stoppingDistance;
        if (rHit.transform != null && rHit.transform.tag != "Player")
        {
            if (rHit.transform.tag == "Ground")
            {
                nma.stoppingDistance = 0.1f;
                if (destinationDistance <= 0.1f)
                {
                    anim.SetBool("Run", false);
                    nma.Stop();
                }
            }
            else
            {
                stoppingDistance = GetEntityStopRadius(rHit.transform);
                nma.stoppingDistance = stoppingDistance;

                if (destinationDistance <= stoppingDistance)
                {
                    anim.SetBool("Run", false);
                    nma.Stop();
                }
            }
        }
    }

    void MoveToPosition()
    {
        nma.destination = rHit.point;
        anim.SetBool("Run", true);
        nma.Resume();
    }

    float GetEntityStopRadius(Transform entity)
    {
        return entity.GetComponent().StoppingDistance;
    }

 

K řádkům anim.SetBool( "Run" , true ); a anim.SetBool( "Run" , false ); se ještě vrátím v následujícím logu.