Hitchhiking the HoloToolkit-Unity, Leg 8–Bit of Fun with a Teleporting Rabbit

NB: The usual blog disclaimer for this site applies to posts around HoloLens. I am not on the HoloLens team. I have no details on HoloLens other than what is on the public web and so what I post here is just from my own experience experimenting with pieces that are publicly available and you should always check out the official developer site for the product documentation.

It’s just before Xmas and I thought I’d have a bit of fun in trying to take the ideas around world anchors that I started to play with here;

Hitchhiking the HoloToolkit-Unity, Leg 5–Baby Steps with World Anchors and Persisting Holograms

and see if I could experiment with anchoring holograms across rooms in my house rather than just anchoring holograms in one room.

Most demos can benefit a lot from the introduction of a rabbit Winking smile and I found this one in the Unity Asset Store going free and so I borrowed him;

image

and tried to introduce him to the idea of teleporting between platforms that are anchored into real-world space;

Sketch

I was keen to see if I could put those platforms in different rooms in the real world to see how that works.

I feel like there’s something quite ‘magical’ about taking a hologram in one room and doing something to it such that it appears in another room that I can’t currently see (maybe it’s obscured by a wall, floor, ceiling). An extra bit of magic might be to have multiple users sharing the same rooms and holograms so that two users in separate rooms could send holograms back and forth between them and there’s support in the HoloToolkit for doing that type of sharing (see the ‘Holograms 240’ tutorial here).

Below is a quick recording of the simple demo that I came up with – note that;

  1. The video is fairly low quality.
  2. I’ve chosen to have the spatial mapping grid displayed here as an aid to debugging whereas usually I wouldn’t want to use this type of app with all those grey lines drawn all over the mesh like this and it’s a simple ‘checkbox’ change to switch it off.

How does that look from a Unity/code perspective?

Code Behind the Video

My Unity scene is very simple in that I have installed the HoloToolkit and configured my project, scene and capabilities for Holographic development (including making sure that my app has the capability for ‘Spatial Perception’).

My scene then has 4 things in it;

image

The camera is just the default camera and the Cursor is the cursor prefab taken from the HoloToolkit as I did in this video and with no modifications made to it.

When it comes to SpatialMapping, I started with the prefab from the toolkit as in this video but then I did some reading about caching and settings and multiple rooms and I found all of these forum posts really useful;

and that last article is so good that I’ll reference it twice;

System’s Spaces vs in-app mapping

but for my scenario I went with what seemed like a fairly simple set-up in that I took the Spatial Mapping prefab;

image

and I took away the ‘Object Surface Observer’ as it didn’t seem to have a runtime role to play and I then tweaked some parameters down to try and end up with a situation where I was asking for a less complex mesh than the prefab’s setting of 500 triangles per square metre (mine is set to 50) and trying to update it more frequently (1 second versus 3.5 seconds);

image

and I think that’s all I changed on it although I also edited the material that the wireframe is rendered with to make it more of a grey than a white because I find that so much shiny white mesh can make my head hurt Smile 

Prefabs to Instantiate at Runtime

I made a prefab out of my ‘teleporter’ disk so as to make it an empty game object which contains a box collider and a cylinder and it uses the Hand Draggable script in order that the user can drag it around trying to make sure that it does not turn to face the user but, instead, stays upright;

image

The intention of the empty game object is to provide a parent object that can be used to reparent the rabbit as it teleports to this particular disk and that seemed to work out reasonably well.

The rabbit works in a very similar way in that it is also a pre-fab and I have attached both a capsule collider and the Hand Draggable script to it as well except this time around I allow it to orientate itself towards the user;

image

and both of these prefabs also have a script on them called Drag and Collide which really came from my previous blog post where I’d done something very similar with a Xmas present. That script looks like this;

using System;
using HoloToolkit.Unity.InputModule;
using UnityEngine;
using HoloToolkit.Unity;

public class DragAndCollide : MonoBehaviour
{
  void Start()
  {
    var handDraggable = this.gameObject.GetComponent<HandDraggable>();

    handDraggable.StartedDragging += OnStartedDragging;
    handDraggable.StoppedDragging += OnStoppedDragging;
  }
  void OnStartedDragging()
  {
    // Transporters (not rabbits) have world anchors.
    if (GameState.Instance.IsTransporter(this.gameObject))
    {
      WorldAnchorManager.Instance.RemoveAnchor(this.gameObject);
    }

    // We add rigid body here if it's not already present. We can't
    // leave it all the time because it's not compatible with
    // world anchors so we can't have both at the same time
    // (AFAIK).
    var rigidBody = this.gameObject.GetComponent<Rigidbody>();

    if (rigidBody == null)
    {
      rigidBody = this.gameObject.AddComponent<Rigidbody>();

      // We don't want our objects to fall to the floor.
      rigidBody.useGravity = false;
    }
  }
  void OnStoppedDragging()
  {
    // We take away the RigidBody because it doesn't play well with
    // world anchor. Can't use DestroyImmediate here because that's
    // not allowed (AFAIK).
    Destroy(this.gameObject.GetComponent<Rigidbody>());

    // Empirically, if I try and create the world anchor at this
    // point then I seem to hit a problem whereas if I just set
    // a flag then do it in LateUpdate things seem to work out.
    if (GameState.Instance.IsTransporter(this.gameObject))
    {
      this.gameObjectNeedsAnchoring = true;
    }
  }
  void OnCollisionEnter(Collision collision)
  {
    // If we do get a collision then we (a bit rudely) just stop the
    // drag operation.
    this.gameObject.GetComponent<HandDraggable>().StopDragging();

    // Did we collide into a transporter?
    if (GameState.Instance.IsTransporter(collision.collider.gameObject) &&
      (!GameState.Instance.IsPlacingTransporters))
    {
      // Transport!
      GameState.Instance.Transport(this.gameObject, collision.collider.gameObject);
    }
  }
  private void LateUpdate()
  {
    if (this.gameObjectNeedsAnchoring)
    {
      this.gameObjectNeedsAnchoring = false;

      WorldAnchorManager.Instance.AttachAnchor(
        this.gameObject,
        GameState.Instance.GetTransporterName(this.gameObject));
    }
  }
  bool gameObjectNeedsAnchoring;
}

That script is really trying to achieve a few things;

  1. It tries to make sure that the game object has a RigidBody component on it between Start/Stop of drags.
  2. It tries to remove the world anchor from a transporter as it starts being dragged and put it back when it stops being dragged.
  3. It tries to detect collisions between rabbits and transporters and to transport the rabbit when those collisions occur.

This script depends on a script called GameState and you might notice that I’m using what I think of as quite a ‘clunky’ mechanism to try and differentiate in this script between rabbits and transporters using a method GameState.Instance.IsTransporter and this somewhat reflects my collision with Unity and its approach to adding components to objects rather than (e.g.) sub-classing them. I could do a better job than I’m currently doing so this way of doing things does feel a little ‘rough’.

Those are my 2 prefabs and the behaviours on them. I then have a ‘placeholder’ in my scene which does some more work.

The Placeholder and GameState Script

My placeholder has a bunch of scripts associated with it, all but one of which are straight out of the HoloToolkit without any modification to the settings;

image

in the screenshot, all the red bits are just scripts used without any changes from the toolkit. The blue stuff is where I have added my own script (GameState) and you’d spot that it has two properties for the Transporter Prefab and the Character Prefab (the rabbit) and also that I’ve used the Keyword Manager in order to link the Clean spoken keyword to a GameState.OnClear method.

That GameState script then looks like this;

using HoloToolkit.Unity;
using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class GameState : Singleton<GameState>, IInputClickHandler
{
  public Transform transporterPrefab;
  public Transform characterPrefab;
 
  public GameObject TransporterHome
  {
    get;set;
  }
  public GameObject TransporterAway
  {
    get;set;
  }

  public bool IsPlacingTransporters
  {
    get
    {
      return ((this.TransporterHome == null) ||
        (this.TransporterAway == null));
    }
  }
  public bool IsTransporter(GameObject gameObject)
  {
    return ((gameObject == this.TransporterHome) ||
      (gameObject == this.TransporterAway));
  }
  public string GetTransporterName(GameObject gameObject)
  {
    return (gameObject == 
      this.TransporterHome ? transporterHomeName : transporterAwayName);
  }
  public void Transport(GameObject gameObject, GameObject transporter)
  {
    // Are we going 'home' or 'away' ?
    var newTransporter =
      (transporter == this.TransporterHome) ? this.TransporterAway : this.TransporterHome;

    // Reparent the object (rabbit) off the new transporter and move it up in the
    // air a little so that it no longer collides because if it collides then my
    // other code will stop it moving!
    gameObject.transform.SetParent(newTransporter.transform);
    gameObject.transform.localPosition = new Vector3(0, 0.2f, 0);
  }
  void Start()
  {
    // We want to respond to clicks if nothing else handles them.
    InputManager.Instance.PushFallbackInputHandler(this.gameObject);
  }
  private void Update()
  {
    if (!this.loaded && (WorldAnchorManager.Instance.AnchorStore != null))
    {
      var ids = WorldAnchorManager.Instance.AnchorStore.GetAllIds();

      // NB: I'm assuming that the ordering here is either preserved or
      // maybe doesn't matter.
      foreach (var id in ids)
      {
        var instance = Instantiate(this.transporterPrefab);
        instance.SetParent(this.transform);

        WorldAnchorManager.Instance.AttachAnchor(instance.gameObject, id);

        if (id == transporterHomeName)
        {
          this.TransporterHome = instance.gameObject;
        }
        else
        {
          this.TransporterAway = instance.gameObject;
        }
      }
      this.loaded = true;
    }
  }
  public void OnInputClicked(InputEventData eventData)
  {
    // Are we making a transporter or a rabbit?
    var prefab = 
      this.IsPlacingTransporters ? this.transporterPrefab : this.characterPrefab;

    var instance = Instantiate(prefab);

    // Initially, parent the object off the placeholder.
    instance.SetParent(this.transform);

    // Try to put it 2m in front of the user (which might cause it to collide which would
    // be 'bad' 😦 ).
    instance.gameObject.transform.position =
      GazeManager.Instance.GazeOrigin +
      GazeManager.Instance.GazeNormal * 2.0f;

    if (!this.IsPlacingTransporters)
    {
      instance.gameObject.transform.forward = GazeManager.Instance.GazeNormal;
    }
    else
    {
      if (this.TransporterHome == null)
      {
        this.TransporterHome = instance.gameObject;
      }
      else
      {
        this.TransporterAway = instance.gameObject;
      }
    }
  }
  public void OnClear()
  {
    foreach (Transform child in this.transform)
    {
      // Only transporters have world anchors so remove those here.
      if (this.IsTransporter(child.gameObject))
      {
        WorldAnchorManager.Instance.RemoveAnchor(child.gameObject);
      }
      Destroy(child.gameObject);
    }
  }
  bool loaded;
  const string transporterHomeName = "home";
  const string transporterAwayName = "away";
}

and while that code might be a little bit ‘clumsy’, I don’t think there’s too much in there that shouldn’t be fairly obvious in terms of what it’s doing and the code around loading world anchors is something that I’ve re-used from a previous blog post.

I think it’s fairly clear that the way in which I’m identifying my 2 transporter objects is pretty clunky and there’s perhaps a need for some other classes to manage (e.g.) the world anchors but this was my first rough sketch to try and get what I wanted working.

Wrapping Up

I quite enjoyed playing with this scenario – I wanted to see how well world anchors work for me across more than one room and they turned out to work very well indeed and I learned a few things (mostly from the forums) along the way so it was useful and I also got a rabbit that could teleport Winking smile