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:
- [DSP2017] 18# Holowizja - zabiłem pierwsze zombi w pokoju
- [DSP2017] 20# Holowizja - przyszli po mnie do pokoju, ale ich zabijam
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 HoloToolkit. Ostatnia 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:
- Hitchhiking the HoloToolkit-Unity, Leg 3–Spatial Understanding (& Mapping)
- Hitchhiking the HoloToolkit-Unity, Leg 14–More with Spatial Understanding
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:
- HoloLens Tutorial – Spatial Mapping
- HoloLens Tutorial – Object Surface Observer
- HoloLens Tutorial – TagAlongs and Billboarding
Teraz mogłem zająć się esencją zawartą w:
- HoloLens Tutorial – Spatial Understanding
- HoloLens Tutorial – Finalize Spatial Understanding
- HoloLens Tutorial – Object Placement and Scaling
- HoloLens Tutorial – Hologram Management
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.
Chodzę sobie i po chwili zaczynam dostawać bieżące dane o jakości skanowania.
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ę.
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.
Jak to wygląda od strony dewelopera? W Unity powstała następująca scena:
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.
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 –Można powiedzieć, że powstał tłum jak na jakimś marszu czy manifestacji, ale pamiętajmy że to tylko hologramy.
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 – Stay tuned.
Brak komentarzy:
Prześlij komentarz