Last Updated: Jul 3, 2017
Tutorial Accomplishments
- Colliders
- Add a Character with a Rigidbody
- Animate the Character
- Add Character Movement
This is the final section of this tutorial. This pulls in the last fundamental features for a HoloLens application, physics and collision detection. The completed application has been published to the Windows Store with some additional polish, but essentially every step of its creation has been documented in this tutorial. Let’s wrap this tutorial up!
Colliders
Colliders in Unity are exactly what they sound like, they are used to detect collisions. In the case of this tutorial we are going to have a character with a Rigidbody and allow the Unity physics engine to detect collisions sot that we can respond accordingly. We have already added colliders, but lets review where we added them and why. The first place we have colliders is on the Spatial Understanding Mesh:
This checkbox tells spatial understanding to add a collider to the mesh as it is being created. We want a collider on the mesh for multiple reasons. The first reason is to make sure the character has something to walk on. Because we are using the physics engine to move the character if there was no collider on the floor, gravity would pull the character down and he would fall infinitely. We also want the objects in the room to have colliders so that if the character runs into these objects he can stop instead of running through them.
The trees and building also get colliders. These were a bit more difficult to implement and you can see them in ObjectCollectionManager.cs. In CreateTree there is a line that adds a mesh collider to the object as it is instantiated:
In Create Building there is a line that calls a method that adds colliders to all of the children of the GameObject:
This is required because the buildings are made up of nested GameObjects and each GameObject must get its own collider. We are adding these colliders to the buildings and trees so that the Character can not run through them but instead hits them and stops. A key point is that we are applying colliders to the real world objects and the holographic objects for the exact same reason. This is Mixed Reality so we want the interaction between a hologram and the real world to be the same as a hologram and a hologram!
We already have all of this in place so no need to make any changes, we were planning this all along…
NOTE: The colliders I’ve used in this tutorial are detailed and expensive (mesh colliders), for a real world application you would want to use box or sphere colliders to improve application performance. That would be a lot more busy work and not the point of the tutorial, so for the sake of the tutorial we took the quickest route. There are many good resources available on optimizing colliders for unity, any of them would be applicable to HoloLens development as well.
Add a Character with a Rigidbody
I’m going to use a free character from the Unity 3D Asset Store. If you want to follow along exactly I recommend using the same character. I’m using the Low Poly Cowboy available here:
This is a nice looking character that is low poly and is easily handled by the HoloLens. Import the asset into your application including everything:
You now have a Cowboy folder in your assets. We are going to want to make some changes to the Cowboy.prefab later on, so for now let’s make a copy named Movable Cowboy. Drag the cowboy.prefab into your hierarchy. Rename the “Cowboy” gameobject in the Hierarchy to “Movable Cowboy”. Drag the Movable Cowboy from the hierarchy back to your Project pane. Delete the Movable Cowboy from the Hierarchy. Your project should now look like this:
Next we will add some code that knows how to instantiate the cowboy. Edit ObjectCollectionManager.cs to contain the following code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using HoloToolkit.Unity; | |
using UnityEngine; | |
public class ObjectCollectionManager : Singleton<ObjectCollectionManager> | |
{ | |
[Tooltip("A collection of square building prefabs to generate in the world.")] | |
public List<GameObject> SquareBuildingPrefabs; | |
[Tooltip("The desired size of square buildings in the world.")] | |
public Vector3 SquareBuildingSize = new Vector3(.5f, .5f, .5f); | |
[Tooltip("A collection of Wide building prefabs to generate in the world.")] | |
public List<GameObject> WideBuildingPrefabs; | |
[Tooltip("The desired size of wide buildings in the world.")] | |
public Vector3 WideBuildingSize = new Vector3(1.0f, .5f, .5f); | |
[Tooltip("A collection of tall building prefabs to generate in the world.")] | |
public List<GameObject> TallBuildingPrefabs; | |
[Tooltip("The desired size of tall buildings in the world.")] | |
public Vector3 TallBuildingSize = new Vector3(.25f, .05f, .25f); | |
[Tooltip("A collection of tree prefabs to generate in the world.")] | |
public List<GameObject> TreePrefabs; | |
[Tooltip("The desired size of trees in the world.")] | |
public Vector3 TreeSize = new Vector3(.25f, .5f, .25f); | |
[Tooltip("Will be calculated at runtime if is not preset.")] | |
public float ScaleFactor; | |
[Tooltip("Cowboy to Display.")] | |
public GameObject Cowboy; | |
[Tooltip("Cowboy desired Size.")] | |
public Vector3 CowboySize; | |
public List<GameObject> ActiveHolograms = new List<GameObject>(); | |
public void CreateSquareBuilding(int number, Vector3 positionCenter, Quaternion rotation) | |
{ | |
CreateBuilding(SquareBuildingPrefabs[number], positionCenter, rotation, SquareBuildingSize); | |
} | |
public void CreateTallBuilding(int number, Vector3 positionCenter, Quaternion rotation) | |
{ | |
CreateBuilding(TallBuildingPrefabs[number], positionCenter, rotation, TallBuildingSize); | |
} | |
public void CreateWideBuilding(int number, Vector3 positionCenter, Quaternion rotation) | |
{ | |
CreateBuilding(WideBuildingPrefabs[number], positionCenter, rotation, WideBuildingSize); | |
} | |
private void CreateBuilding(GameObject buildingToCreate, 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(buildingToCreate, 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(buildingToCreate); | |
AddMeshColliderToAllChildren(newObject); | |
ActiveHolograms.Add(newObject); | |
} | |
} | |
public void CreateTree(int number, Vector3 positionCenter, Quaternion rotation) | |
{ | |
// Stay center in the square but move down to the ground | |
var position = positionCenter – new Vector3(0, TreeSize.y * .5f, 0); | |
GameObject newObject = Instantiate(TreePrefabs[number], 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(TreePrefabs[number]); | |
newObject.AddComponent<MeshCollider>(); | |
ActiveHolograms.Add(newObject); | |
} | |
} | |
public void CreateCowboy(Vector3 positionCenter, Quaternion rotation) | |
{ | |
// Stay center in the square but move down to the ground | |
var position = positionCenter – new Vector3(0, CowboySize.y * .5f, 0); | |
GameObject newObject = Instantiate(Cowboy, 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 = RescaleToDesiredSizeProportional(Cowboy, CowboySize); | |
newObject.AddComponent<Cowboy>(); | |
ActiveHolograms.Add(newObject); | |
} | |
} | |
private void AddMeshColliderToAllChildren(GameObject obj) | |
{ | |
for (int i = 0; i < obj.transform.childCount; i++) | |
{ | |
obj.transform.GetChild(i).gameObject.AddComponent<MeshCollider>(); | |
} | |
} | |
private Vector3 RescaleToSameScaleFactor(GameObject objectToScale) | |
{ | |
// ReSharper disable once CompareOfFloatsByEqualityOperator | |
if (ScaleFactor == 0f) | |
{ | |
CalculateScaleFactor(); | |
} | |
return objectToScale.transform.localScale * ScaleFactor; | |
} | |
private Vector3 RescaleToDesiredSizeProportional(GameObject objectToScale, Vector3 desiredSize) | |
{ | |
float scaleFactor = CalcScaleFactorHelper(new List<GameObject> { objectToScale }, desiredSize); | |
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(WideBuildingPrefabs, WideBuildingSize); | |
if (ratio < maxScale) | |
{ | |
maxScale = ratio; | |
} | |
ScaleFactor = maxScale; | |
} | |
private float CalcScaleFactorHelper(List<GameObject> objects, Vector3 desiredSize) | |
{ | |
float maxScale = float.MaxValue; | |
foreach (var obj in objects) | |
{ | |
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; | |
} | |
} |
I’ve added two new properties to ObjectCollectionManager, one to specify the model to use for the cowboy and the other to specify the desired size for the cowboy. I’ve also added a CreateCowboy method, which works exactly the same as the other Create methods, except that it adds a script called “Cowboy” to the gameobject as it is spawned. This doesn’t exist yet, so create a new script called Cowboy.cs in the scripts folder.
Open the project in Unity and select Placement in the Hierarchy. The ObjectCollectionManager component now shows the two new properties that we added. Navigate to Assets > Cowboy > Prefabs in the Project pane. Drag Movable Cowboy into the Cowboy property of the visible component. Modify Cowboy size to contain .15 for X, Y, and Z. Your properties should now look like this in the inspector:
Now let’s make updates to spawn the cowboy during world generation. Start by updating PlacementQuery.cs to add Cowboy to the Enum:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using HoloToolkit.Unity; | |
using UnityEngine; | |
public enum ObjectType | |
{ | |
SquareBuilding, | |
WideBuilding, | |
TallBuilding, | |
Tree, | |
Tumbleweed, | |
Mine, | |
Cowboy | |
} | |
public struct PlacementQuery | |
{ | |
public PlacementQuery( | |
SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition placementDefinition, | |
Vector3 dimensions, | |
ObjectType objType, | |
List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule> placementRules = null, | |
List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementConstraint> placementConstraints = null) | |
{ | |
PlacementDefinition = placementDefinition; | |
PlacementRules = placementRules; | |
PlacementConstraints = placementConstraints; | |
Dimensions = dimensions; | |
ObjType = objType; | |
} | |
public readonly SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition PlacementDefinition; | |
public readonly Vector3 Dimensions; | |
public readonly ObjectType ObjType; | |
public readonly List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule> PlacementRules; | |
public readonly List<SpatialUnderstandingDllObjectPlacement.ObjectPlacementConstraint> PlacementConstraints; | |
} |
Next Update ObjectPlacer.cs:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using HoloToolkit.Unity; | |
using UnityEngine; | |
public class ObjectPlacer : MonoBehaviour | |
{ | |
public bool DrawDebugBoxes = false; | |
public bool DrawCowboy = true; | |
public bool DrawBuildings = true; | |
public bool DrawTrees = true; | |
public SpatialUnderstandingCustomMesh SpatialUnderstandingMesh; | |
public Material OccludedMaterial; | |
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; | |
HideGridEnableOcclulsion(); | |
_timeToHideMesh = false; | |
} | |
if (DrawDebugBoxes) | |
{ | |
_boxDrawing.UpdateBoxes(_lineBoxList); | |
} | |
} | |
private void HideGridEnableOcclulsion() | |
{ | |
//SpatialUnderstandingMesh.DrawProcessedMesh = false; | |
SpatialUnderstandingMesh.MeshMaterial = OccludedMaterial; | |
} | |
public void CreateScene() | |
{ | |
// Only if we're enabled | |
if (!SpatialUnderstanding.Instance.AllowSpatialUnderstanding) | |
{ | |
return; | |
} | |
SpatialUnderstandingDllObjectPlacement.Solver_Init(); | |
SpatialUnderstandingState.Instance.SpaceQueryDescription = "Generating World"; | |
List<PlacementQuery> queries = new List<PlacementQuery>(); | |
if (DrawCowboy) | |
{ | |
queries.AddRange(AddCowboy()); | |
} | |
if (DrawBuildings) | |
{ | |
queries.AddRange(AddBuildings()); | |
} | |
if (DrawTrees) | |
{ | |
queries.AddRange(AddTrees()); | |
} | |
GetLocationsFromSolver(queries); | |
} | |
public List<PlacementQuery> AddCowboy() | |
{ | |
return CreateLocationQueriesForSolver(1, ObjectCollectionManager.Instance.CowboySize, ObjectType.Cowboy); | |
} | |
public List<PlacementQuery> AddBuildings() | |
{ | |
var queries = CreateLocationQueriesForSolver(ObjectCollectionManager.Instance.WideBuildingPrefabs.Count, ObjectCollectionManager.Instance.WideBuildingSize, ObjectType.WideBuilding); | |
queries.AddRange(CreateLocationQueriesForSolver(ObjectCollectionManager.Instance.SquareBuildingPrefabs.Count, ObjectCollectionManager.Instance.SquareBuildingSize, ObjectType.SquareBuilding)); | |
queries.AddRange(CreateLocationQueriesForSolver(ObjectCollectionManager.Instance.TallBuildingPrefabs.Count, ObjectCollectionManager.Instance.TallBuildingSize, ObjectType.TallBuilding)); | |
return queries; | |
} | |
public List<PlacementQuery> AddTrees() | |
{ | |
var queries = CreateLocationQueriesForSolver(ObjectCollectionManager.Instance.TreePrefabs.Count, ObjectCollectionManager.Instance.TreeSize, ObjectType.Tree); | |
return queries; | |
} | |
private int _placedSquareBuilding; | |
private int _placedTallBuilding; | |
private int _placedWideBuilding; | |
private int _placedTree; | |
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.SquareBuilding: | |
ObjectCollectionManager.Instance.CreateSquareBuilding(_placedSquareBuilding++, toPlace.Position, rotation); | |
break; | |
case ObjectType.TallBuilding: | |
ObjectCollectionManager.Instance.CreateTallBuilding(_placedTallBuilding++, toPlace.Position, rotation); | |
break; | |
case ObjectType.WideBuilding: | |
ObjectCollectionManager.Instance.CreateWideBuilding(_placedWideBuilding++, toPlace.Position, rotation); | |
break; | |
case ObjectType.Tree: | |
ObjectCollectionManager.Instance.CreateTree(_placedTree++, toPlace.Position, rotation); | |
break; | |
case ObjectType.Cowboy: | |
ObjectCollectionManager.Instance.CreateCowboy(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>(); | |
if (objType == ObjectType.Cowboy) | |
{ | |
placementConstraints.Add(SpatialUnderstandingDllObjectPlacement.ObjectPlacementConstraint.Create_NearCenter()); | |
} | |
SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition placementDefinition = SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition.Create_OnFloor(halfBoxDims); | |
placementQueries.Add( | |
new PlacementQuery(placementDefinition, | |
boxFullDims, | |
objType, | |
placementRules, | |
placementConstraints | |
)); | |
} | |
return placementQueries; | |
} | |
} |
These changes are consistent with the already established patterns. The first addition is a boolean that allows us to turn the cowboy on and off, we default this to true. Next in CreateScene we add a check to see if this is true and if it is, we add a request for a place to put the Cowboy. We add an AddCowboy method which is called by CreateScene to create the correct query for the Cowboy. Then we updated ProcessPlacementResults to add a case for ObjectType.Cowboy that calls the ObjectCollectionManager code from above to instantiate the Cowboy. Finally we add some logic to CreateLocationQueriesForSolver to notice when it is placing a cowboy and add a special constraint to add him near the middle of the room, which will also be near the middle of the scene, where the user will naturally look.
Build and Deploy your application. Notice that the cowboy is instantiated in the center of your room and he is standing in the T pose.
Animate the Character
Next we will add animation to the character to get him out of the T pose. To do this we will use one of the animations that is a part of the Unity Standard assets. Open your project in unity. Select from the menu Assets > Import Package > Characters. Uncheck the root node to deselect everything, we only want a small part of this asset pack. Find the third person character part of the tree. Check Animation and Animator so that your import dialog looks like this:
Click import and wait for the assets to be imported. Find the Standard Assets folder in your project pane. Drill down to Assets > Standard Assets > Characters > ThirdPersonCharacter > Animator. This is the standard animator provided for use in third person games. This will be used to animate your character, double click on it to see how it works. In particular pay attention to the parameters, this is what we will use to trigger animations for our character:
Navigate in the project pane to the Assets > Cowboy > Prefabs folder and select the Movable Cowboy. In the inspector select the Controller property and Assign ThirdPartyAnimatorController.
Build and Deploy your application. Notice that the cowboy is now performing the idle animation which is more natural than the T pose.
Add Character Movement
We need the Cowboy to listen to the HoloLens Select gesture so we know when and were the user wants to move them. Edit the file Cowboy.cs and add the following content:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using HoloToolkit.Unity.InputModule; | |
using UnityEngine; | |
public class Cowboy : MonoBehaviour, IInputClickHandler | |
{ | |
private Animator _anim; | |
private void Start() | |
{ | |
InputManager.Instance.AddGlobalListener(gameObject); | |
} | |
public void MoveCharacterToPoint(Vector3 newLoc) | |
{ | |
if (_anim == null) _anim = gameObject.GetComponent<Animator>(); | |
_anim.SetFloat("Forward", 1); | |
} | |
public void OnInputClicked(InputClickedEventData eventData) | |
{ | |
MoveCharacterToPoint(GazeManager.Instance.HitPosition); | |
} | |
} |
This code implements the IInputClickHandler similar to what we did in a previous tutorial. When the user performs the select gesture, MoveCharacterToPoint is called and passed the location that the user had the cursor on when they performed the select gesture. We then set the Animator component Forward variable to 1, which causes the character to animate forward.
Build and Deploy your application. Perform the select gesture in the world. The cowboy animates forward! The provided run animation looks pretty good, but the cowboy just runs forward in the direction he was facing and runs forever.
Now we will make the character move to the actual location that the user selected. Edit Cowboy.cs again with the following contents:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using HoloToolkit.Unity.InputModule; | |
using UnityEngine; | |
public class Cowboy : MonoBehaviour, IInputClickHandler | |
{ | |
public float WalkSpeed = 1f; | |
private float _timeStartedLerping; | |
private float _ground; | |
private bool _walking; | |
private float _timeToWalk; | |
private Vector3 _start; | |
private Vector3 _destination; | |
private Animator _anim; | |
private Rigidbody _rigidbody; | |
private void Start() | |
{ | |
InputManager.Instance.AddGlobalListener(gameObject); | |
_rigidbody = GetComponent<Rigidbody>(); | |
_ground = transform.position.y; | |
} | |
void LateUpdate() | |
{ | |
if (_walking) | |
{ | |
if (MoveForFrame(_destination)) | |
{ | |
StopWalking(); | |
} | |
} | |
} | |
public void MoveCharacterToPoint(Vector3 newLoc) | |
{ | |
if (_anim == null) _anim = gameObject.GetComponent<Animator>(); | |
// Don't let the cowboy leave the plane that he is on | |
newLoc.y = _ground; | |
_destination = newLoc; | |
StartWalking(); | |
} | |
private void StartWalking() | |
{ | |
_start = transform.position; | |
_timeStartedLerping = Time.time; | |
_timeToWalk = Vector3.Distance(_start, _destination) / WalkSpeed; | |
_walking = true; | |
_anim.SetFloat("Forward", 1); | |
} | |
private void StopWalking() | |
{ | |
_walking = false; | |
_anim.SetFloat("Forward", 0); | |
} | |
private bool MoveForFrame(Vector3 destination) | |
{ | |
float timeSinceStarted = Time.time – _timeStartedLerping; | |
float percentageComplete = timeSinceStarted / _timeToWalk; | |
_rigidbody.MovePosition(Vector3.Lerp(_start, destination, percentageComplete)); | |
if (percentageComplete >= 1.0f) | |
{ | |
// done turning | |
return true; | |
} | |
return false; | |
} | |
public void OnInputClicked(InputClickedEventData eventData) | |
{ | |
MoveCharacterToPoint(GazeManager.Instance.HitPosition); | |
} | |
} |
We’ve added a ton of the logic necessary for movement into this script. In Start we grab a handle to the Cowboy’s Rigidbody and make a note of where the ground is. For this app we don’t ever want the cowboy to leave the ground so we store the ground value for use when calculating where the cowboy should move to. In MoveChracaterToPoint we set the private destination variable to the value the user selected and also set the Y value to the location of the ground. This makes sure no matter where the user selects, that the cowboy stays on the ground. This method calls the new StartWalking method which keeps track of the starting position, when we started moving the character from the start position to the destination, and how long it should take the character to walk there based on the desired walking speed. We start the walking animation and set the walking state to true. LateUpdate picks up from here and does two things, if we are walking it calls MoveForFrame and it checks to see if we are at our destination. If we are at our destination it calls StopWalking. MoveForFrame uses the information saved during start walking to determine where the character should be for this frame and lerp them there. It also lets the caller know if we are finished moving. StopWalking stops the walking animation and sets the walking state to false.
Before this code will work we need to give the Cowboy a RigidBody. Open Unity and select the Movable Cowboy in the project pane. Click add component and select Rigidbody. Click add component and select Capsule Collider. Set the properties to the following settings:
The Rigidbody is used by the physics engine to give the character realistic interaction with the environment. We freeze the rotation in the X and Z direction so that the cowboy doesn’t fall over since he is top heavy. (Remember gravity is being applied once we add the Rigidbody). We also configure Continuous Dynamic collision detection to improve the accuracy of collision detection. Rgidbody’s also require a properly configured collider. We are not allowed to use a mesh collider like we do in the rest of the tutorial so we added a Capsule collider. The properties are configured to cause the capsule to encompass the majority of the character. This is closer to the correct way to optimize colliders, but for a real world project I would recommend multiple colliders for a character so that the entire model is covered.
Build and Deploy your application. Perform the select gesture in the world. The cowboy now moves to the selected location and stops when he gets there! A huge improvement but we still have a couple problems. The cowboy runs sideways or backwards because we haven’t turned him to face the correct direction. He also runs through objects, where is the collision detection I promised?
Let’s fix the turning problem first. Make the following changes to Cowboy.cs:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using HoloToolkit.Unity.InputModule; | |
using UnityEngine; | |
public class Cowboy : MonoBehaviour, IInputClickHandler | |
{ | |
public float WalkSpeed = 1f; | |
public float MarginOfError = .1f; | |
public float TimeToTurn = .75f; | |
private float _timeStartedLerping; | |
private float _ground; | |
private bool _turning; | |
private Quaternion _startRotation; | |
private Quaternion _targetRotation; | |
private bool _walking; | |
private float _timeToWalk; | |
private Vector3 _start; | |
private Vector3 _destination; | |
private Animator _anim; | |
private Rigidbody _rigidbody; | |
private void Start() | |
{ | |
InputManager.Instance.AddGlobalListener(gameObject); | |
_rigidbody = GetComponent<Rigidbody>(); | |
_ground = transform.position.y; | |
} | |
void LateUpdate() | |
{ | |
if (_turning) | |
{ | |
if (TurnForFrame(_startRotation)) | |
{ | |
StopTurning(); | |
StartWalking(); | |
} | |
} | |
if (_walking) | |
{ | |
if (MoveForFrame(_destination)) | |
{ | |
StopWalking(); | |
} | |
} | |
} | |
public void MoveCharacterToPoint(Vector3 newLoc) | |
{ | |
if (_anim == null) _anim = gameObject.GetComponent<Animator>(); | |
// Don't let the cowboy leave the plane that he is on | |
newLoc.y = _ground; | |
_destination = newLoc; | |
StartTurning(newLoc); | |
} | |
private void StartWalking() | |
{ | |
_start = transform.position; | |
_timeStartedLerping = Time.time; | |
_timeToWalk = Vector3.Distance(_start, _destination) / WalkSpeed; | |
_walking = true; | |
_anim.SetFloat("Forward", 1); | |
} | |
private void StopWalking() | |
{ | |
_walking = false; | |
_anim.SetFloat("Forward", 0); | |
} | |
private bool MoveForFrame(Vector3 destination) | |
{ | |
float timeSinceStarted = Time.time – _timeStartedLerping; | |
float percentageComplete = timeSinceStarted / _timeToWalk; | |
_rigidbody.MovePosition(Vector3.Lerp(_start, destination, percentageComplete)); | |
if (percentageComplete >= 1.0f) | |
{ | |
// done turning | |
return true; | |
} | |
return false; | |
} | |
private void StartTurning(Vector3 locationToFace) | |
{ | |
_startRotation = transform.rotation; | |
_targetRotation = Quaternion.LookRotation(locationToFace – transform.position); | |
_timeStartedLerping = Time.time; | |
_anim.SetFloat("Turn", 1); | |
_turning = true; | |
} | |
private void StopTurning() | |
{ | |
_anim.SetFloat("Turn", 0); | |
_turning = false; | |
} | |
private bool TurnForFrame(Quaternion startRotation) | |
{ | |
float timeSinceStarted = Time.time – _timeStartedLerping; | |
float percentageComplete = timeSinceStarted / TimeToTurn; | |
transform.rotation = Quaternion.Slerp(startRotation, _targetRotation, percentageComplete); | |
if (percentageComplete >= 1.0f) | |
{ | |
// done turning | |
return true; | |
} | |
return false; | |
} | |
public void OnInputClicked(InputClickedEventData eventData) | |
{ | |
MoveCharacterToPoint(GazeManager.Instance.HitPosition); | |
} | |
} |
We’ve updated MoveCharacterToPoint to call StartTurning instead of StartWalking. StartTurning stores the starting rotation and the destination location, it also stores the time we started turning the character, and enables the turn animation. Finally it sets the turning state to true. LateUpdate now also checks to see if we are turning, and if we are it calls TurnForFrame as well as calling StopTurning and StartWalking once we have reached our target rotation. TurnForFrame works just like walk for frame figuring out based on the time elapsed where we should be in rotating and rotating the character that direction.
Fixing collision detection is very easy because the collision detection is already taking place and we are ignoring it currently. Edit Cowboy.cs to contain the following changes:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using HoloToolkit.Unity.InputModule; | |
using UnityEngine; | |
public class Cowboy : MonoBehaviour, IInputClickHandler | |
{ | |
public float WalkSpeed = 1f; | |
public float MarginOfError = .1f; | |
public float TimeToTurn = .75f; | |
private float _timeStartedLerping; | |
private float _ground; | |
private bool _turning; | |
private Quaternion _startRotation; | |
private Quaternion _targetRotation; | |
private bool _walking; | |
private float _timeToWalk; | |
private Vector3 _start; | |
private Vector3 _destination; | |
private Animator _anim; | |
private Rigidbody _rigidbody; | |
private void Start() | |
{ | |
InputManager.Instance.AddGlobalListener(gameObject); | |
_rigidbody = GetComponent<Rigidbody>(); | |
_ground = transform.position.y; | |
} | |
void LateUpdate() | |
{ | |
if (_turning) | |
{ | |
if (TurnForFrame(_startRotation)) | |
{ | |
StopTurning(); | |
StartWalking(); | |
} | |
} | |
if (_walking) | |
{ | |
if (MoveForFrame(_destination)) | |
{ | |
StopWalking(); | |
} | |
} | |
} | |
void OnCollisionEnter(Collision collision) | |
{ | |
foreach (var contact in collision.contacts) | |
{ | |
// ignore collisions in the Y direction | |
if (!AlmostEquals(contact.point.y, _ground, MarginOfError)) | |
{ | |
StopWalking(); | |
_rigidbody.freezeRotation = true; | |
} | |
} | |
} | |
public void MoveCharacterToPoint(Vector3 newLoc) | |
{ | |
if (_anim == null) _anim = gameObject.GetComponent<Animator>(); | |
// Don't let the cowboy leave the plane that he is on | |
newLoc.y = _ground; | |
_destination = newLoc; | |
StartTurning(newLoc); | |
} | |
private void StartWalking() | |
{ | |
_start = transform.position; | |
_timeStartedLerping = Time.time; | |
_timeToWalk = Vector3.Distance(_start, _destination) / WalkSpeed; | |
_walking = true; | |
_anim.SetFloat("Forward", 1); | |
} | |
private void StopWalking() | |
{ | |
_walking = false; | |
_anim.SetFloat("Forward", 0); | |
} | |
private bool MoveForFrame(Vector3 destination) | |
{ | |
float timeSinceStarted = Time.time – _timeStartedLerping; | |
float percentageComplete = timeSinceStarted / _timeToWalk; | |
_rigidbody.MovePosition(Vector3.Lerp(_start, destination, percentageComplete)); | |
if (percentageComplete >= 1.0f) | |
{ | |
// done turning | |
return true; | |
} | |
return false; | |
} | |
private void StartTurning(Vector3 locationToFace) | |
{ | |
_startRotation = transform.rotation; | |
_targetRotation = Quaternion.LookRotation(locationToFace – transform.position); | |
_timeStartedLerping = Time.time; | |
_anim.SetFloat("Turn", 1); | |
_turning = true; | |
} | |
private void StopTurning() | |
{ | |
_anim.SetFloat("Turn", 0); | |
_turning = false; | |
} | |
private bool TurnForFrame(Quaternion startRotation) | |
{ | |
float timeSinceStarted = Time.time – _timeStartedLerping; | |
float percentageComplete = timeSinceStarted / TimeToTurn; | |
transform.rotation = Quaternion.Slerp(startRotation, _targetRotation, percentageComplete); | |
if (percentageComplete >= 1.0f) | |
{ | |
// done turning | |
return true; | |
} | |
return false; | |
} | |
private static bool AlmostEquals(float double1, float double2, float precision) | |
{ | |
return Math.Abs(double1 – double2) <= precision; | |
} | |
public void OnInputClicked(InputClickedEventData eventData) | |
{ | |
MoveCharacterToPoint(GazeManager.Instance.HitPosition); | |
} | |
} |
OnCollisionEnter is called anytime the physics engine detects that the cowboy’s rigidbody has hit a collider. We examine all of the collisions and ignores any collision with the ground. We call stop walking if we hit something that isn’t the ground. We also freeze rotation to eliminate the occasional spin in place the cowboy will do when he hits some invisible part of the mesh.
Build and Deploy your application. Perform the select gesture in the world. The cowboy now turns to face the direction he is going before walking there, he no longer spins in place when stopping, and he stops when he runs into something. We’ve used the unity physics engine and animation engine to create a character that moves realistically through the world.
Finished
This is the end of this tutorial series, I will work on getting the complete code published. Please let me know what you would like to see next!
Tutorial Index
Versions: Unity 2017.1.0p5 | MixedRealityToolkit-Unity v1.2017.1.0 | Visual Studio 2017 15.3.2Unity 3D Project Creation | How to create a HoloLens project in Unity 3D |
Source Control | Configure git for HoloLens / Unity work |
Spatial Mapping | How to spatial map a Room |
Object Surface Observer | Set up fake spatial map data for the Unity editor |
TagAlongs and Billboarding | Tag along instructions to the user to force text on screen |
Spatial Understanding | Add spatial understanding to get play space detail |
Finalizing Spatial Understanding | Complete Spatial Understanding and Input Manager |
Object Placement and Scaling | Find valid locations for holograms in the play space |
Hologram Management | Manage the holograms you want to place in the world |
Cursor and Voice | Add a cursor and voice commands |
Occlusion | Add occlusion with the real world to your scene |
Colliders and Rigidbodys | Add Colliders and RigidBodys to your holograms |
- Download the completed app Western Town in the Windows Store!
- Completed Source code from the entire tutorial available on GitHub.
With the limited tutorials available for HoloLens, thanks for sharing your app.
Thank you for sharing! 🙂