czwartek, 11 maja 2017

[DSP2017] 22# HoloSurvivalShooter - skanuję pokój i na podłodze ustawiam zombi

Moja wersja gry Survival Shooter na HoloLens wzbogaciła się o kolejny ważny punkt z listy to do, a mianowicie chodzi o dynamiczne dodawanie zombi na scenę. Z tym punktem wiąże się dodatkowo jedna duża rzecz, której nie umieściłem na tej liście. Do tej pory wrzuciłem na sztywno kilka zombi z podaniem im współrzędnych, by sprawiały wrażenie, że są mniej więcej na podłodze (narysowanej dzięki spatial mapping). Dziś podejdziemy do tego w prawidłowy sposób. Skorzystamy z analizy skanowanej przestrzeni i będziemy zlecać umieszczanie obiektów na podłodze w inteligentny sposób.

Dla porządku dodam, że rozwój mini gry HoloSurvivalShooter został przedstawiony w dwóch ostatnich postach o HoloLens:

Projekt z tamtego okresu przeniosłem do gałęzi HoloSurvivalShooter1, natomiast dzisiejsza bardziej zaawansowana postać trafiła do HoloSurvivalShooter.

Skąd czerpałem wiedzę o tym jak rozpoznawać logicznie skanowaną przestrzeń i umieszczać na jej powierzchniach hologramy?

Zacząłem od dokumentacji, której wcześniej nie odwiedziłem:

W dokumentacji znajdziemy odwołanie do przykładu SpatialUnderstanding w HoloToolkitOstatnia wersja HoloToolkit nie zawiera już przykładów w swoim pakiecie dla Unity, ale znajdują się one nadal w źródłach. 

Aha, dodam że od tego razu zacząłem korzystać z ostatniej nowszej wersji HoloToolkit oraz przeszedłem na Unity 5.6. Ta wersja Unity wnosi jedną bardzo ciekawą rzecz, którą już testowałem, ale o tym może innym razem. Wróćmy do analizowania przestrzeni.

Mike Taulty poruszył tę tematykę w postach:

Zwłaszcza ostatni jest interesujący, wyjaśnia trzewia API. Okazuje się, że tak naprawdę za rozmieszczanie obiektów odpowiada niezarządzana biblioteka w C++, do której napisane są wrappery. Autor zmodyfikował nawet tę bibliotekę, ale nie jest to potrzebne w moim przypadku.

Poszukiwałem opisu przykładu z HoloToolkit, aby go zrozumieć i wykorzystać jakiś jego podzbiór, by zamiast sześcianów umieszczać zombi na podłodze. Trochę szukałem i w końcu natrafiłem na genialny tutorial na stronie http://www.cameronvetter.com/tag/tutorial/. Autor opisuje w nim krok po kroku, co robi, a także tłumaczy pisany przez siebie kod, a nawet objaśnia różne funkcje z API biblioteki dostarczanej w HoloToolkit. Moim zdaniem takie coś powinno znaleźć się w Holographic Academy. Przygotowałem sobie wstępnie scenę na nowym pustym projekcie wykonując:

Teraz mogłem zająć się esencją zawartą w:

Do tak powstałego projektu przeniosłem część obiektów sceny, zasoby i skrypty z dotychczasowej wersji gry. Do szczegółów wrócę poniżej, teraz pokażę jak to działa.

Po odpaleniu w emulatorze pojawia się skrawek siatki oraz napis informujący, by sobie trochę pochodzić po pokoju w celu zeskanowania przestrzeni do gry.

HVG00

Chodzę sobie i po chwili zaczynam dostawać bieżące dane o jakości skanowania.

HVG01

Gdy pojawi się żółty napis informujący, że system jest gotowy, możemy “kliknąć” i zakończyć skanowanie otaczającej nas przestrzeni. Oznacza to, że osiągnęliśmy jakość skanu wystarczającą do rozpoczęcia gry. Można sobie więcej pochodzić, by jeszcze poprawić siatkę.

HVG1

Po “kliknięciu” dane skanowania zostały ukryte (przy korzystaniu z HoloLens w prawdziwym pokoju siatka też powinna raczej zostać ukryta). Zostały za to rozmieszczone 3 zombi na podłodze, po chwili pojawią się nastęne 3 itd. Pojawił się też licznik punktów. Jak najadę kursorem na zombi i “kliknę” to oddam celny strzał. Po kilku takich strzałach go zabiję, on upadnie i zniknie, a ja - jak ostatnio - zdobędę za niego punkty.

HVG2

Jak to wygląda od strony dewelopera? W Unity powstała następująca scena:

HVU1

Pod obiekt HoloLensCamera jak poprzednio podpięty jest skrypt  PlayerShooting, w którym dokonałem małej zmiany. Uzależniłem oddanie strzału od tego, czy zakończyło się skanowanie przestrzeni (czy dane o skanowaniu są ukryte):

public void OnInputClicked(InputClickedEventData eventData)
    {
        if(timer >= timeBetweenBullets && Time.timeScale != 0 && SpatialUnderstandingState.Instance.HideText)
        {
            Shoot ();
        }
    }

Pole ScoreText ma kanał alfa na pełną przezroczystość, by na początku w trakcie skanowania nie było widoczne. Tak jak ostatnio jest podpięty do niego skrypt ScoreManager. Oczywiście zombi też mają swój skrypt jak poprzednio.

Najbardziej kluczowe są skrypty ObjectCollectionManager i ObjectPlacer. Pierwszy jest zbiorem moetod do dynamicznego dodawanie obiektów zombi na podłodze. Drugi korzysta z tych metod.

HVU8

Skrypt ObjectCollectionManager jest wzorowany na tym z tutoriala, ale przerobiłem go tak, by rysował zombi, a nie budynki i drzewa.

public class ObjectCollectionManager : Singleton<ObjectCollectionManager>
{

    [Tooltip("Zombunny prefab.")]
    public GameObject ZombunnyPrefab;

    [Tooltip("The desired size of Zombunny.")]
    public Vector3 ZombunnySize = new Vector3(.5f, 1.0f, .5f);

    [Tooltip("ZomBear prefabs.")]
    public GameObject ZomBearPrefab;

    [Tooltip("The desired size of ZomBear.")]
    public Vector3 ZomBearSize = new Vector3(.5f, 1.0f, .5f);

    [Tooltip("Hellephant prefabs.")]
    public GameObject HellephantPrefab;

    [Tooltip("The desired size of Hellephant.")]
    public Vector3 HellephantSize = new Vector3(1.2f, 1.2f, 1.2f);   

    [Tooltip("Will be calculated at runtime if is not preset.")]
    public float ScaleFactor;
   

    public void CreateZombunny(Vector3 positionCenter, Quaternion rotation)
    {
        CreateEnemy(ZombunnyPrefab, positionCenter, rotation, ZombunnySize);
    }

    public void CreateZomBear(Vector3 positionCenter, Quaternion rotation)
    {
        CreateEnemy(ZomBearPrefab, positionCenter, rotation, ZomBearSize);
    }

    public void CreateHellephant(Vector3 positionCenter, Quaternion rotation)
    {
        CreateEnemy(HellephantPrefab, positionCenter, rotation, HellephantSize);
    }

    private void CreateEnemy(GameObject enemyToCreate, Vector3 positionCenter, Quaternion rotation, Vector3 desiredSize)
    {
        // Stay center in the square but move down to the ground
        var position = positionCenter - new Vector3(0, desiredSize.y * .5f, 0);

        GameObject newObject = Instantiate(enemyToCreate, position, rotation);

        if (newObject != null)
        {
            // Set the parent of the new object the GameObject it was placed on
            newObject.transform.parent = gameObject.transform;

            newObject.transform.localScale = RescaleToSameScaleFactor(enemyToCreate);           
        }
    }

    private Vector3 RescaleToSameScaleFactor(GameObject objectToScale)
    {
        // ReSharper disable once CompareOfFloatsByEqualityOperator
        if (ScaleFactor == 0f)
        {
            CalculateScaleFactor();
        }

        return objectToScale.transform.localScale * ScaleFactor;
    }

    private Vector3 StretchToFit(GameObject obj, Vector3 desiredSize)
    {
        var curBounds = GetBoundsForAllChildren(obj).size;

        return new Vector3(desiredSize.x / curBounds.x / 2, desiredSize.y, desiredSize.z / curBounds.z / 2);
    }

    private void CalculateScaleFactor()
    {
        float maxScale = float.MaxValue;

        var ratio = CalcScaleFactorHelper(HellephantPrefab, HellephantSize);
        if (ratio < maxScale)
        {
            maxScale = ratio;
        }

        ScaleFactor = maxScale;
    }

    private float CalcScaleFactorHelper(GameObject obj, Vector3 desiredSize)
    {
        float maxScale = float.MaxValue;
       
        var curBounds = GetBoundsForAllChildren(obj).size;
        var difference = curBounds - desiredSize;

        float ratio;

        if (difference.x > difference.y && difference.x > difference.z)
        {
            ratio = desiredSize.x / curBounds.x;
        }
        else if (difference.y > difference.x && difference.y > difference.z)
        {
            ratio = desiredSize.y / curBounds.y;
        }
        else
        {
            ratio = desiredSize.z / curBounds.z;
        }

        if (ratio < maxScale)
        {
            maxScale = ratio;
        }       

        return maxScale;
    }

    private Bounds GetBoundsForAllChildren(GameObject findMyBounds)
    {
        Bounds result = new Bounds(Vector3.zero, Vector3.zero);

        foreach (var curRenderer in findMyBounds.GetComponentsInChildren<Renderer>())
        {
            if (result.extents == Vector3.zero)
            {
                result = curRenderer.bounds;
            }
            else
            {
                result.Encapsulate(curRenderer.bounds);
            }
        }

        return result;
    }
}

W skrypcie ObjectPlacer w stosunku do tutoriala dodałem pole ScoreText oraz parametr spawnTime. Mając referencję na ScoreText ustawiam na nim biały kolor (z pełnym kanałem alfa) po tym jak zakończy się skanowanie i zostaną ukryte informacje na temat jego jakości. SpawnTime to okres na stworzenie nowego zombi i okres przerwy do następnego tworzenia. Zamiast wszystko zbudować raz za pomocą metody CreateScene, przerobiłem to tak, że wydzieliłem logikę odpowiedzialną za dodanie obiektów do metody CreateSceneObjects, po czym zlecam powtarzanie jej wykonywania za pomocą standardowego w Unity wywołania InvokeRepeating.

public class ObjectPlacer : MonoBehaviour
{
    public bool DrawDebugBoxes = false;
    public bool DrawEnemies = true;   

    public SpatialUnderstandingCustomMesh SpatialUnderstandingMesh;

    public Text ScoreText;

    public float spawnTime = 12f;

    private readonly List<BoxDrawer.Box> _lineBoxList = new List<BoxDrawer.Box>();

    private readonly Queue<PlacementResult> _results = new Queue<PlacementResult>();

    private bool _timeToHideMesh;
    private BoxDrawer _boxDrawing;

    // Use this for initialization
    void Start()
    {
        if (DrawDebugBoxes)
        {
            _boxDrawing = new BoxDrawer(gameObject);
        }

    }

    void Update()
    {
        ProcessPlacementResults();

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

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

    }

    private void HideGridEnableOcclulsion()
    {
        //SpatialUnderstandingMesh.DrawProcessedMesh = false;
    }

    public void CreateScene()
    {
        // Only if we're enabled
        if (!SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
        {
            return;
        }

        SpatialUnderstandingDllObjectPlacement.Solver_Init();

        SpatialUnderstandingState.Instance.SpaceQueryDescription = "Generating World";

        InvokeRepeating("CreateSceneObjects", spawnTime, spawnTime);
    }

    private void CreateSceneObjects()
    {
        List<PlacementQuery> queries = new List<PlacementQuery>();

        if (DrawEnemies)
        {
            queries.AddRange(AddEnemies());
        }

        GetLocationsFromSolver(queries);
    }

    public List<PlacementQuery> AddEnemies()
    {

        var queries = CreateLocationQueriesForSolver(1, ObjectCollectionManager.Instance.ZombunnySize, ObjectType.Zombunny);
        queries.AddRange(CreateLocationQueriesForSolver(1, ObjectCollectionManager.Instance.ZomBearSize, ObjectType.ZomBear));
        queries.AddRange(CreateLocationQueriesForSolver(1, ObjectCollectionManager.Instance.HellephantSize, ObjectType.Hellephant));
        return queries;
    }       

    private void ProcessPlacementResults()
    {
        if (_results.Count > 0)
        {
            var toPlace = _results.Dequeue();
            // Output
            if (DrawDebugBoxes)
            {
                DrawBox(toPlace, Color.red);
            }

            var rotation = Quaternion.LookRotation(toPlace.Normal, Vector3.up);

            switch (toPlace.ObjType)
            {
                case ObjectType.Zombunny:
                    ObjectCollectionManager.Instance.CreateZombunny(toPlace.Position, rotation);
                    break;
                case ObjectType.ZomBear:
                    ObjectCollectionManager.Instance.CreateZomBear(toPlace.Position, rotation);
                    break;
                case ObjectType.Hellephant:
                    ObjectCollectionManager.Instance.CreateHellephant(toPlace.Position, rotation);
                    break;              
            }
        }
    }

    private void DrawBox(PlacementResult boxLocation, Color color)
    {
        if (boxLocation != null)
        {
            _lineBoxList.Add(
                new BoxDrawer.Box(
                    boxLocation.Position,
                    Quaternion.LookRotation(boxLocation.Normal, Vector3.up),
                    color,
                    boxLocation.Dimensions * 0.5f)
            );
        }
    }

    private void GetLocationsFromSolver(List<PlacementQuery> placementQueries)
    {
#if UNITY_WSA && !UNITY_EDITOR
        System.Threading.Tasks.Task.Run(() =>
        {
            // Go through the queries in the list
            for (int i = 0; i < placementQueries.Count; ++i)
            {
                var result = PlaceObject(placementQueries[i].ObjType.ToString() + i,
                                         placementQueries[i].PlacementDefinition,
                                         placementQueries[i].Dimensions,
                                         placementQueries[i].ObjType,
                                         placementQueries[i].PlacementRules,
                                         placementQueries[i].PlacementConstraints);
                if (result != null)
                {
                    _results.Enqueue(result);
                }
            }

            _timeToHideMesh = true;
        });
#else
        _timeToHideMesh = true;
#endif
    }

    private PlacementResult PlaceObject(string placementName,
        SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition placementDefinition,
        Vector3 boxFullDims,
        ObjectType objType,
        List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule> placementRules = null,
        List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementConstraint> placementConstraints = null)
    {

        // New query
        if (SpatialUnderstandingDllObjectPlacement.Solver_PlaceObject(
                placementName,
                SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(placementDefinition),
                (placementRules != null) ? placementRules.Count : 0,
                ((placementRules != null) && (placementRules.Count > 0)) ? SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(placementRules.ToArray()) : IntPtr.Zero,
                (placementConstraints != null) ? placementConstraints.Count : 0,
                ((placementConstraints != null) && (placementConstraints.Count > 0)) ? SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(placementConstraints.ToArray()) : IntPtr.Zero,
                SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticObjectPlacementResultPtr()) > 0)
        {
            SpatialUnderstandingDllObjectPlacement.ObjectPlacementResult placementResult = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticObjectPlacementResult();

            return new PlacementResult(placementResult.Clone() as SpatialUnderstandingDllObjectPlacement.ObjectPlacementResult, boxFullDims, objType);
        }

        return null;
    }

    private List<PlacementQuery> CreateLocationQueriesForSolver(int desiredLocationCount, Vector3 boxFullDims, ObjectType objType)
    {
        List<PlacementQuery> placementQueries = new List<PlacementQuery>();

        var halfBoxDims = boxFullDims * .5f;

        var disctanceFromOtherObjects = halfBoxDims.x > halfBoxDims.z ? halfBoxDims.x * 3f : halfBoxDims.z * 3f;

        for (int i = 0; i < desiredLocationCount; ++i)
        {
            var placementRules = new List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule>
            {
                SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule.Create_AwayFromOtherObjects(disctanceFromOtherObjects)               
            };

            var placementConstraints = new List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementConstraint>();

            SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition placementDefinition = SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition.Create_OnFloor(halfBoxDims);

            placementQueries.Add(
                new PlacementQuery(placementDefinition,
                    boxFullDims,
                    objType,
                    placementRules,
                    placementConstraints
                ));
        }

        return placementQueries;
    }

}

Tak się rozpisałem, że zapomniałem wyłączyć emulator HoloLens i namnożyło się w nim trochę tych zombi –SmileMożna powiedzieć, że powstał tłum jak na jakimś marszu czy manifestacji, ale pamiętajmy że to tylko hologramy.

HVG3

Trzeba dodać, że teraz zombi są jeszcze niegroźne, bo nie idą do mnie po podłodze pokoju i nie gryzą przy zbliżeniu. Nie tracę więc życia nic nie robiąc w grze, a zabijać je mogę. Następnym razem już tak lightowo być nie może –Smile Stay tuned.

Brak komentarzy: