sobota, 20 maja 2017

[DSP 2017] 26# HoloSurvivalShooter - zombi maszerują po podłodze wprost na mnie!

Dziś jest jeden z najlepszych dni jeśli chodzi o DSP. Oto kolejny znaczący postęp w mojej holograficznej wersji gry Survival Shooter z Unity. Zombi mogą teraz chodzić po podłodze pokoju, a celem ich jestem ja, nikt inny.

Weźmy na przykład różowego misia, który rodzi się gdzieś koło kanapy i zaczyna iść na mnie…

ale zdążyłem go zastrzelić, uff…

Generalnie jednak nie ma lekko. Nowe zombi ciągle się rodzą i dość szybko zostałem osaczony… Ale walczę, kilka z nich zabiłem…

Jak do tego doszedłem od strony technicznej? Nie było bardzo łatwo, ale lektura dwóch japońskich czy chińskich postów (po przetłumaczeniu przez Google Chrome na angielski) znacznie mi pomogła. Ale po kolei.

Do Unity 5.6 znajdowanie ścieżki i nawigacja do celu w HoloLens wymagała własnego szukania rozwiązań z implementacją A*. Przy okazji przypomniały mi się czasy z trzeciego roku studiów, gdzie trzeba było implementować wyjście z losowo generowanych labiryntów ze zwierciadłami i wtedy jeden kolega polecił mi ten algorytm, a ja w celu jego poznania nabyłem dość grubą książkę, która stoi do tej pory na półce, ale program w oparciu o nią zadziałał i nawet rozwiązał zadowalajacą liczbę plansz na zaliczenie… 

Ale wróćmy do roku 2017 i rzeczywistości mieszanej.  Nie musiałem tym razem wracać - przynajmniej bezpośrednio - do A*, ponieważ udało się zastosować nawigację z NavMeshAgent znaną od dawna w Unity i zastosowaną w oryginale gry Survival Shooter. Jednak tam w czasie edycji generuje się powierzchnię, po której będą poruszać się agenci. W HoloLens podłoga pokoju powstaje dynamicznie w trakcie działania. Dopiero Unity 5.6 wprowadziło możliwość definiowania nawigacji z użyciem NavMeshAgent na dynamicznie tworzonej powierzchni, a i tak wysokopoziomowe komponenty nie weszły do zasadniczego API i zostały udostępnione w postaci przykładów na github pod adresem https://github.com/Unity-Technologies/NavMeshComponents (podobne przykłady były też dla Unity 5.5, ale nie są już wspierane).

Przykłady Unity nie są adresowane konkretnie na HoloLens, tylko ogólnie. Warto się nimi pobawić. Można je odpalać bezpośrednio w Unity. Do szybkiego zrozumienia wystarczy choćby 2_drop_plank.

Capture2

Za dynamiczną generację powierzchni dla agentów odpowiada skrypt LocalNavMeshBuilder przypięty do pustego obiektu Game o takiej samej nazwie. Do generowania wyszukiwane są obiekty z przypiętymi skryptami NavMeshSourceTag.

Jak to zaadoptować do HoloLens?  Nie jest to udokumentowane, ale natrafiłem na dwa genialne posty z Dalekiego Wschodu:

Koniecznie warto je przyczytać. Opisane rozwiązania jak najbardziej działają, co też sprawdziłem budując je na czystych projektach. Wracając jednak do ostatniej wersji mojej gry, to tam używam SpatialUnderstanding razem ze SpatialMapping z HoloLens Toolkit, a nie tylko samego SpatialMapping. Ostatecznie owszem posiłkowałem się miksem rozwiązań z powyższych linków, ale wprowadziłem pewne zmiany.

Po pierwsze mając siatkę ze SpatialUnderstanding nie łapało mi agentów na siatce budowanej bezpośrednio ze SpatialMapping, której już nie wyświetlałem. Popatrzyłem jednak na kod SpatialUnderstanding, kod z postów i wywnioskowałem, że mogę w sumie dynamicznie dodawać obiekty NavMeshSourceTag nie tylko do SpatialMapping, ale i do SpatialUnderstanding, bo obydwa zawierają obiekty wywodzące się z obiektu zarządzającego płaszczyznami. Koniec końców wykonałem następujące czynności:

Na SpatialMappingObserver w SpatialMapping zwiększyłem dokładność odwzorowania do 2000 trójkątów (może nie jest to konieczne w tym przypadku)

Dodałem pusty obiekt Game o nazwie NavigationManager, do którego podpiąłem skrypty LocalNavMeshBuilder (z przykładów Unity) oraz SpatialMappingNavMesh (z postów z Dalekiego Wschodu).

image

Ten ostatni zamiast ze SpatialMapping powiązałem ze SpatialUnderstanding. Zwróćmy uwagę na jego kod, który w elegancki sposób pozwala podpiąć się do obiektu zarządzajacego powierzchniami, by dodać do nich elementy NavMeshSourceTag. Na ich podstawie z kolei LocalNavMeshBuilder stworzy powierzchnie dla wędrówek agentów:

public class SpatialMappingNavMesh : MonoBehaviour
{
    public GameObject SpatialMapping;

    private void Awake()
    {
        var spatialMappingSources = SpatialMapping.GetComponents<SpatialMappingSource>();
        foreach (var source in spatialMappingSources)
        {
            source.SurfaceAdded += SpatialMappingSource_SurfaceAdded;
            source.SurfaceUpdated += SpatialMappingSource_SurfaceUpdated;
        }
    }

    void Start()
    {
    }

    private void SpatialMappingSource_SurfaceAdded(object sender, DataEventArgs<SpatialMappingSource.SurfaceObject> e)
    {
        e.Data.Object.AddComponent<NavMeshSourceTag>();
    }

    private void SpatialMappingSource_SurfaceUpdated(object sender, DataEventArgs<SpatialMappingSource.SurfaceUpdate> e)
    {
        var navMeshSourceTag = e.Data.New.Object.GetComponent<NavMeshSourceTag>();
        if (navMeshSourceTag == null)
        {
            e.Data.New.Object.AddComponent<NavMeshSourceTag>();
        }
    }
}

Do prefabrykatów w prefabs (Zombunny, ZomBear i Hellephant) dodałem elementy NavMeshAgent (o parametrach nieco zmienionych w stosunku do oryginału):

image  image  image

i odpowiednio zmodyfikowany skrypt EnemyMovement:

public class EnemyMovement : MonoBehaviour
{
    //Transform player;
    //PlayerHealth playerHealth;
    EnemyHealth enemyHealth;
    UnityEngine.AI.NavMeshAgent nav;
    Camera mainCamera;

    void Awake ()
    {
        //player = GameObject.FindGameObjectWithTag ("Player").transform;       
        //playerHealth = player.GetComponent <PlayerHealth> ();
        mainCamera = Camera.main;
        enemyHealth = GetComponent <EnemyHealth> ();
        nav = GetComponent <UnityEngine.AI.NavMeshAgent> ();
    }


    void Update ()
    {
        if (enemyHealth.currentHealth > 0 /*&& playerHealth.currentHealth > 0*/)
        {
            //nav.SetDestination (player.position);

            RaycastHit hit;
            var headPosition = mainCamera.transform.position;
            //var gazeDirection = mainCamera.transform.forward;
            var downDirection = new Vector3(0, -1, 0);

            if (Physics.Raycast(headPosition, downDirection, out hit))
            {
                nav.SetDestination(hit.point);
            }
        }
        else
        {
            nav.enabled = false;
        }
    }
}

Wychodzi tu druga dość istotna sprawa, a mianowicie kwestia celu. Podążanie postaci za moim wzrokiem owszem działa, ale nie jest to chyba najbardziej odpowiedni wariant dla mojej gry. Ja, kamerzysta, chcę być osaczany tam, gdzie stoję, a więc niezależnie od tego, gdzie w danym momencie spojrzę. Rozwiązałem to poprzez znalezienie punktu przecięcia z powierzchnią (podłogą) dla wektora zapoczątkowanego w położeniu kamery i skierowanego pionowo w dół.

Ustawiłem też tak jak w oryginale zakładkę Navigation, ale nie wiem czy to ma wpływ na dynamicznie generowaną płaszczyznę dla agentów.

W miarę to nawet działa, stwory lezą do moich stóp. Trochę trudno obserwować to w hełmie na głowie, ale w realu w sumie też by tak to wyglądało. Co ciekawe mogą chodzić nie tylko po podłodze, ale i po kanapie za mną przejdą, jakbym tam się znalazł. Generalnie kod z dodawaniem  NavMeshSourceTag dotyczy każdej zeskanowanej powierzchni, nie ogranicza się stricte tylko do podłogi.

Dla jeszcze większego zbliżenia z klimatem oryginału dodałem na scenę także znany z niego obiekt BackgroundMusic oraz plik audio z nim związany. Jedyną rzeczą, jaką dorobiłem, to wyłączenie automatycznego odtwarzania muzyczki od momentu uruchomienia aplikacji. U mnie następuje to po wygenerowaniu sceny. W tym celu w skrypcie ObjectPlacer w obiekcie Placement dodałem pole na AudioSource, które wiążę w Unity z elementem BackgroundMusic,

image

oraz dopisałem linijkę odnoszącą się do BackgroundMusic w metodzie:

void Update()
    {
        ProcessPlacementResults();

        if (_timeToHideMesh)
        {
            SpatialUnderstandingState.Instance.HideText = true;
            ScoreText.color = Color.white;
            HideGridEnableOcclulsion();
            BackgroundMusic.Play();
            _timeToHideMesh = false;
        }

        if (DrawDebugBoxes)
        {
            _boxDrawing.UpdateBoxes(_lineBoxList);
        }

    }

Całość gry w obecnej postaci trafiła tradycyjnie już na github do HoloSurvivalShooter (a poprzednia wersja do HoloSurvivalShooter2).

Patrząc na listę to do tym razem zrobiliśmy punkty 3 i  8, pozostały do realizacji utrata życia przez kamerzystę (4 i 5) i ogień z lufy, czy też palca (7). W najbliższym czasie trzeba będzie pewnie pomyśleć o implementacji utraty życia przez grającego. Stay tuned !

Brak komentarzy: