Compartir a través de


HoloLens (1.ª generación) Uso compartido de 240: varios dispositivos HoloLens

Importante

Los tutoriales de Mixed Reality Academy se diseñaron con HoloLens (1.ª generación), Unity 2017 y Mixed Reality cascos inmersivos en mente. Por lo tanto, creemos que es importante dejar estos tutoriales en su lugar para los desarrolladores que siguen buscando orientación en el desarrollo para esos dispositivos. Estos tutoriales no se actualizarán con los conjuntos de herramientas o interacciones más recientes que se usan para HoloLens 2 y es posible que no sean compatibles con las versiones más recientes de Unity. Se mantendrán para seguir trabajando en los dispositivos compatibles. Se ha publicado una nueva serie de tutoriales para HoloLens 2.

A los hologramas se les da presencia en nuestro mundo si permanecen en su lugar a medida que nos movemos en el espacio. HoloLens mantiene los hologramas en su lugar mediante el uso de varios sistemas de coordenadas para realizar un seguimiento de la ubicación y orientación de los objetos. Cuando compartimos estos sistemas de coordenadas entre dispositivos, podemos crear una experiencia compartida que nos permita participar en un mundo holográfico compartido.

En este tutorial, haremos lo siguiente:

  • Configure una red para una experiencia compartida.
  • Comparta hologramas entre dispositivos HoloLens.
  • Descubra a otras personas en nuestro mundo holográfico compartido.
  • Crea una experiencia interactiva compartida en la que puedes dirigirte a otros jugadores y lanzar proyectiles en ellos.

Compatibilidad con dispositivos

Curso HoloLens Cascos envolventes
MR Sharing 240: varios dispositivos HoloLens ✔️

Antes de empezar

Requisitos previos

Archivos de proyecto

  • Descargue los archivos necesarios para el proyecto. Requiere Unity 2017.2 o posterior.
    • Si todavía necesita compatibilidad con Unity 5.6, use esta versión.
    • Si todavía necesita compatibilidad con Unity 5.5, use esta versión.
    • Si todavía necesita compatibilidad con Unity 5.4, use esta versión.
  • Des archive los archivos en el escritorio u otra ubicación de fácil acceso. Mantenga el nombre de la carpeta como SharedHolograms.

Nota:

Si desea examinar el código fuente antes de descargarlo, está disponible en GitHub.

Capítulo 1: Holo World

En este capítulo, configuraremos nuestro primer proyecto de Unity y recorreremos paso a paso el proceso de compilación e implementación.

Objetivos

  • Configure Unity para desarrollar aplicaciones holográficas.
  • ¡Ve tu holograma!

Instrucciones

  • Inicie Unity.
  • Seleccione Abrir.
  • Escriba location como la carpeta SharedHolograms que anteriormente desarchivaba.
  • Seleccione Nombre del proyecto y haga clic en Seleccionar carpeta.
  • En la jerarquía, haga clic con el botón derecho en la cámara principal y seleccione Eliminar.
  • En la carpeta HoloToolkit-Sharing-240/Prefabs/Camera , busque el prefabricado Cámara principal .
  • Arrastre y coloque la cámara principal en la jerarquía.
  • En la jerarquía, haga clic en Crear y crear vacío.
  • Haga clic con el botón derecho en el nuevo GameObject y seleccione Cambiar nombre.
  • Cambie el nombre de GameObject a HologramCollection.
  • Seleccione el objeto HologramCollection en la jerarquía.
  • En el Inspector , establezca la posición de transformación en: X: 0, Y: -0.25, Z: 2.
  • En la carpeta Hologramas del panel Proyecto, busque el recurso de EnergyHub .
  • Arrastre y coloque el objeto EnergyHub desde el panel Proyecto a la jerarquía como elemento secundario de HologramCollection.
  • Seleccione Guardar > escena como...
  • Asigne a la escena un nombre SharedHolograms y haga clic en Guardar.
  • Presione el botón Reproducir en Unity para obtener una vista previa de los hologramas.
  • Presione Reproducir una segunda vez para detener el modo de vista previa.

Exportación del proyecto de Unity a Visual Studio

  • En Unity, seleccione Configuración de compilación de archivos>.
  • Haga clic en Agregar escenas abiertas para agregar la escena.
  • Seleccione Plataforma universal de Windows en la lista Plataforma y haga clic en Cambiar plataforma.
  • Establezca SDK en Universal 10.
  • Establezca Dispositivo de destino en HoloLens y Tipo de compilación de UWP en D3D.
  • Compruebe Proyectos de C# de Unity.
  • Haga clic en Compilar.
  • En la ventana del explorador de archivos que aparece, cree una nueva carpeta denominada "App".
  • Haga clic en la carpeta Aplicación .
  • Presione Seleccionar carpeta.
  • Cuando Unity haya terminado, aparecerá una ventana de Explorador de archivos.
  • Abra la carpeta Aplicación .
  • Abra SharedHolograms.sln para iniciar Visual Studio.
  • Con la barra de herramientas superior de Visual Studio, cambie el destino de Depurar a Versión y de ARM a X86.
  • Haga clic en la flecha desplegable situada junto a Máquina local y seleccione Dispositivo remoto.
    • Establezca la dirección en el nombre o la dirección IP de HoloLens. Si no conoce la dirección IP del dispositivo, busque en Configuración > Red & Opciones avanzadas de Internet > o pregunte a Cortana "Hola Cortana, ¿Cuál es mi dirección IP?"
    • Deje el modo de autenticación establecido en Universal.
    • Haga clic en Seleccionar
  • Haga clic en Depurar > inicio sin depuración o presione Ctrl + F5. Si es la primera vez que se implementa en el dispositivo, deberá emparejarlo con Visual Studio.
  • Coloque su HoloLens y busque el holograma de EnergyHub.

Capítulo 2: Interacción

En este capítulo, interactuaremos con nuestros hologramas. En primer lugar, agregaremos un cursor para visualizar nuestra mirada. A continuación, agregaremos Gestos y usaremos nuestra mano para colocar nuestros hologramas en el espacio.

Objetivos

  • Use la entrada de mirada para controlar un cursor.
  • Use la entrada de gestos para interactuar con hologramas.

Instrucciones

Mirada

  • En el panel Jerarquía , seleccione el objeto HologramCollection .
  • En el panel Inspector, haga clic en el botón Agregar componente .
  • En el menú, escriba en el cuadro de búsqueda Administrador de miradas. Seleccione el resultado de la búsqueda.
  • En la carpeta HoloToolkit-Sharing-240\Prefabs\Input , busque el recurso Cursor .
  • Arrastre y coloque el recurso Cursor en la jerarquía.

Gesto

  • En el panel Jerarquía , seleccione el objeto HologramCollection .
  • Haga clic en Agregar componente y escriba Administrador de gestos en el campo de búsqueda. Seleccione el resultado de la búsqueda.
  • En el panel Jerarquía, expanda HologramCollection.
  • Seleccione el objeto EnergyHub secundario.
  • En el panel Inspector, haga clic en el botón Agregar componente .
  • En el menú, escriba en el cuadro de búsqueda Ubicación del holograma. Seleccione el resultado de la búsqueda.
  • Para guardar la escena, seleccione Guardar > escena.

Implementación y disfrute

  • Compile e implemente en HoloLens con las instrucciones del capítulo anterior.
  • Una vez que la aplicación se inicie en HoloLens, mueva la cabeza y observe cómo energyHub sigue su mirada.
  • Observe cómo aparece el cursor cuando mira el holograma y cambia a una luz puntual al no mirar un holograma.
  • Realice un toque de aire para colocar el holograma. En este momento en nuestro proyecto, solo puede colocar el holograma una vez (volver a implementarlo para intentarlo de nuevo).

Capítulo 3: Coordenadas compartidas

Es divertido ver e interactuar con hologramas, pero vamos más allá. Configuraremos nuestra primera experiencia compartida: un holograma que todos pueden ver juntos.

Objetivos

  • Configure una red para una experiencia compartida.
  • Establecer un punto de referencia común.
  • Compartir sistemas de coordenadas entre dispositivos.
  • ¡Todos ven el mismo holograma!

Nota:

Las funcionalidades InternetClientServer y PrivateNetworkClientServer deben declararse para que una aplicación se conecte al servidor de uso compartido. Esto ya se hace en Hologramas 240, pero ten esto en cuenta para tus propios proyectos.

  1. En la Editor de Unity, vaya a la configuración del reproductor; para ello, vaya a "Editar > reproductor de configuración del > proyecto".
  2. Haga clic en la pestaña "Tienda Windows".
  3. En la sección "Funcionalidades de configuración > de publicación", compruebe la funcionalidad InternetClientServer y la funcionalidad PrivateNetworkClientServer .

Instrucciones

  • En el panel Proyecto , vaya a la carpeta HoloToolkit-Sharing-240\Prefabs\Sharing .
  • Arrastre y coloque el objeto prefabricado Uso compartido en el panel Jerarquía.

A continuación, es necesario iniciar el servicio de uso compartido. Solo un equipo de la experiencia compartida debe realizar este paso.

  • En Unity, en el menú superior, seleccione el menú HoloToolkit-Sharing-240.
  • Seleccione el elemento Launch Sharing Service (Iniciar servicio de uso compartido) en la lista desplegable.
  • Active la opción Red privada y haga clic en Permitir acceso cuando aparezca el símbolo del firewall.
  • Anote la dirección IPv4 que se muestra en la ventana de la consola del servicio de uso compartido. Se trata de la misma dirección IP en la que se ejecuta el servicio.

Siga el resto de las instrucciones de todos los equipos que se unirán a la experiencia compartida.

  • En Jerarquía, seleccione el objeto Sharing .
  • En el inspector, en el componente Fase de uso compartido , cambie la dirección del servidor de "localhost" a la dirección IPv4 de la máquina que ejecuta SharingService.exe.
  • En Jerarquía , seleccione el objeto HologramCollection .
  • En el Inspector , haga clic en el botón Agregar componente .
  • En el cuadro de búsqueda, escriba Import Export Anchor Manager. Seleccione el resultado de la búsqueda.
  • En el panel Proyecto , vaya a la carpeta Scripts .
  • Haga doble clic en el script HologramPlacement para abrirlo en Visual Studio.
  • Reemplace el contenido por el código siguiente.
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the anchor model.
    /// The anchor model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    private bool animationPlayed = false;

    void Start()
    {
        // We care about getting updates for the anchor transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the anchor transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the anchor if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    void Update()
    {
        if (GotTransform)
        {
            if (ImportExportAnchorManager.Instance.AnchorEstablished &&
                animationPlayed == false)
            {
                // This triggers the animation sequence for the anchor model and 
                // puts the cool materials on the model.
                GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                animationPlayed = true;
            }
        }
        else
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        // Put the anchor 2m in front of the user.
        Vector3 retval = Camera.main.transform.position + Camera.main.transform.forward * 2;

        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the anchor to do its animation and
        // swap its materials.
        if (GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    public void ResetStage()
    {
        // We'll use this later.
    }
}
  • En Unity, seleccione HologramCollection en el panel Jerarquía.
  • En el panel Inspector, haga clic en el botón Agregar componente .
  • En el menú, escriba en el cuadro de búsqueda Administrador de estado de la aplicación. Seleccione el resultado de la búsqueda.

Implementación y disfrute

  • Compile el proyecto para los dispositivos HoloLens.
  • Designe un HoloLens en el que se va a implementar primero. Tendrá que esperar a que el delimitador se cargue en el servicio antes de poder colocar EnergyHub (esto puede tardar aproximadamente entre 30 y 60 segundos). Hasta que finalice la carga, se omitirán los gestos de pulsación.
  • Una vez colocado EnergyHub, su ubicación se cargará en el servicio y, a continuación, se puede implementar en todos los demás dispositivos HoloLens.
  • Cuando un nuevo HoloLens se une por primera vez a la sesión, es posible que la ubicación de EnergyHub no sea correcta en ese dispositivo. Sin embargo, tan pronto como el delimitador y las ubicaciones de EnergyHub se hayan descargado del servicio, EnergyHub debe saltar a la nueva ubicación compartida. Si esto no ocurre en aproximadamente 30-60 segundos, vaya a donde estaba el HoloLens original al establecer el delimitador para recopilar más pistas del entorno. Si la ubicación sigue sin bloquearse, vuelva a implementarla en el dispositivo.
  • Cuando los dispositivos estén listos y ejecuten la aplicación, busque EnergyHub. ¿Pueden estar de acuerdo en la ubicación del holograma y en qué dirección se encuentra el texto?

Capítulo 4: Detección

¡Ahora todos pueden ver el mismo holograma! Ahora vamos a ver a todos los demás conectados a nuestro mundo holográfico compartido. En este capítulo, tomaremos la ubicación principal y la rotación de todos los demás dispositivos HoloLens en la misma sesión de uso compartido.

Objetivos

  • Descubran entre sí en nuestra experiencia compartida.
  • Elige y comparte un avatar de jugador.
  • Adjunte el avatar del jugador junto a la cabeza de todos.

Instrucciones

  • En el panel Proyecto , vaya a la carpeta Hologramas .
  • Arrastre y coloque PlayerAvatarStore en la jerarquía.
  • En el panel Proyecto , vaya a la carpeta Scripts .
  • Haga doble clic en el script AvatarSelector para abrirlo en Visual Studio.
  • Reemplace el contenido por el código siguiente.
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Script to handle the user selecting the avatar.
/// </summary>
public class AvatarSelector : MonoBehaviour
{
    /// <summary>
    /// This is the index set by the PlayerAvatarStore for the avatar.
    /// </summary>
    public int AvatarIndex { get; set; }

    /// <summary>
    /// Called when the user is gazing at this avatar and air-taps it.
    /// This sends the user's selection to the rest of the devices in the experience.
    /// </summary>
    void OnSelect()
    {
        PlayerAvatarStore.Instance.DismissAvatarPicker();

        LocalPlayerManager.Instance.SetUserAvatar(AvatarIndex);
    }

    void Start()
    {
        // Add Billboard component so the avatar always faces the user.
        Billboard billboard = gameObject.GetComponent<Billboard>();
        if (billboard == null)
        {
            billboard = gameObject.AddComponent<Billboard>();
        }

        // Lock rotation along the Y axis.
        billboard.PivotAxis = PivotAxis.Y;
    }
}
  • En Jerarquía , seleccione el objeto HologramCollection .
  • En el inspector , haga clic en Agregar componente.
  • En el cuadro de búsqueda, escriba Administrador de reproductor local. Seleccione el resultado de la búsqueda.
  • En Jerarquía , seleccione el objeto HologramCollection .
  • En el inspector , haga clic en Agregar componente.
  • En el cuadro de búsqueda, escriba Administrador del reproductor remoto. Seleccione el resultado de la búsqueda.
  • Abra el script HologramPlacement en Visual Studio.
  • Reemplace el contenido por el código siguiente.
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the model.
    /// The model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    /// <summary>
    /// When the experience starts, we disable all of the rendering of the model.
    /// </summary>
    List<MeshRenderer> disabledRenderers = new List<MeshRenderer>();

    void Start()
    {
        // When we first start, we need to disable the model to avoid it obstructing the user picking a hat.
        DisableModel();

        // We care about getting updates for the model transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the model transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the model if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    /// <summary>
    /// Turns off all renderers for the model.
    /// </summary>
    void DisableModel()
    {
        foreach (MeshRenderer renderer in gameObject.GetComponentsInChildren<MeshRenderer>())
        {
            if (renderer.enabled)
            {
                renderer.enabled = false;
                disabledRenderers.Add(renderer);
            }
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = false;
        }
    }

    /// <summary>
    /// Turns on all renderers that were disabled.
    /// </summary>
    void EnableModel()
    {
        foreach (MeshRenderer renderer in disabledRenderers)
        {
            renderer.enabled = true;
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = true;
        }

        disabledRenderers.Clear();
    }


    void Update()
    {
        // Wait till users pick an avatar to enable renderers.
        if (disabledRenderers.Count > 0)
        {
            if (!PlayerAvatarStore.Instance.PickerActive &&
            ImportExportAnchorManager.Instance.AnchorEstablished)
            {
                // After which we want to start rendering.
                EnableModel();

                // And if we've already been sent the relative transform, we will use it.
                if (GotTransform)
                {
                    // This triggers the animation sequence for the model and
                    // puts the cool materials on the model.
                    GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                }
            }
        }
        else if (GotTransform == false)
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        // Put the model 2m in front of the user.
        Vector3 retval = Camera.main.transform.position + Camera.main.transform.forward * 2;

        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the model to do its animation and
        // swap its materials.
        if (disabledRenderers.Count == 0 && GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    public void ResetStage()
    {
        // We'll use this later.
    }
}
  • Abra el script AppStateManager en Visual Studio.
  • Reemplace el contenido por el código siguiente.
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Keeps track of the current state of the experience.
/// </summary>
public class AppStateManager : Singleton<AppStateManager>
{
    /// <summary>
    /// Enum to track progress through the experience.
    /// </summary>
    public enum AppState
    {
        Starting = 0,
        WaitingForAnchor,
        WaitingForStageTransform,
        PickingAvatar,
        Ready
    }

    /// <summary>
    /// Tracks the current state in the experience.
    /// </summary>
    public AppState CurrentAppState { get; set; }

    void Start()
    {
        // We start in the 'picking avatar' mode.
        CurrentAppState = AppState.PickingAvatar;

        // We start by showing the avatar picker.
        PlayerAvatarStore.Instance.SpawnAvatarPicker();
    }

    void Update()
    {
        switch (CurrentAppState)
        {
            case AppState.PickingAvatar:
                // Avatar picking is done when the avatar picker has been dismissed.
                if (PlayerAvatarStore.Instance.PickerActive == false)
                {
                    CurrentAppState = AppState.WaitingForAnchor;
                }
                break;
            case AppState.WaitingForAnchor:
                if (ImportExportAnchorManager.Instance.AnchorEstablished)
                {
                    CurrentAppState = AppState.WaitingForStageTransform;
                    GestureManager.Instance.OverrideFocusedObject = HologramPlacement.Instance.gameObject;
                }
                break;
            case AppState.WaitingForStageTransform:
                // Now if we have the stage transform we are ready to go.
                if (HologramPlacement.Instance.GotTransform)
                {
                    CurrentAppState = AppState.Ready;
                    GestureManager.Instance.OverrideFocusedObject = null;
                }
                break;
        }
    }
}

Implementación y disfrute

  • Compile e implemente el proyecto en los dispositivos HoloLens.
  • Cuando escuches un sonido de ping, busca el menú de selección de avatar y selecciona un avatar con el gesto de pulsación de aire.
  • Si no observa ningún holograma, la luz de punto alrededor del cursor cambiará de color cuando HoloLens se comunique con el servicio: inicialización (púrpura oscuro), descarga del delimitador (verde), importación o exportación de datos de ubicación (amarillo) y carga del delimitador (azul). Si la luz de punto alrededor del cursor es el color predeterminado (púrpura claro), entonces usted está listo para interactuar con otros jugadores en su sesión!
  • Mira a otras personas conectadas a tu espacio- habrá un robot holográfico flotando sobre su hombro e imitando sus movimientos de cabeza!

Capítulo 5: Colocación

En este capítulo, haremos que el anclaje pueda colocarse en superficies del mundo real. Usaremos coordenadas compartidas para colocar ese delimitador en el punto intermedio entre todos los usuarios conectados a la experiencia compartida.

Objetivos

  • Coloca hologramas en la malla de asignación espacial en función de la posición de la cabeza de los jugadores.

Instrucciones

  • En el panel Proyecto , vaya a la carpeta Hologramas .
  • Arrastre y coloque el objeto prefabricado CustomSpatialMapping en la jerarquía.
  • En el panel Proyecto , vaya a la carpeta Scripts .
  • Haga doble clic en el script AppStateManager para abrirlo en Visual Studio.
  • Reemplace el contenido por el código siguiente.
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Keeps track of the current state of the experience.
/// </summary>
public class AppStateManager : Singleton<AppStateManager>
{
    /// <summary>
    /// Enum to track progress through the experience.
    /// </summary>
    public enum AppState
    {
        Starting = 0,
        PickingAvatar,
        WaitingForAnchor,
        WaitingForStageTransform,
        Ready
    }

    // The object to call to make a projectile.
    GameObject shootHandler = null;

    /// <summary>
    /// Tracks the current state in the experience.
    /// </summary>
    public AppState CurrentAppState { get; set; }

    void Start()
    {
        // The shootHandler shoots projectiles.
        if (GetComponent<ProjectileLauncher>() != null)
        {
            shootHandler = GetComponent<ProjectileLauncher>().gameObject;
        }

        // We start in the 'picking avatar' mode.
        CurrentAppState = AppState.PickingAvatar;

        // Spatial mapping should be disabled when we start up so as not
        // to distract from the avatar picking.
        SpatialMappingManager.Instance.StopObserver();
        SpatialMappingManager.Instance.gameObject.SetActive(false);

        // On device we start by showing the avatar picker.
        PlayerAvatarStore.Instance.SpawnAvatarPicker();
    }

    public void ResetStage()
    {
        // If we fall back to waiting for anchor, everything needed to
        // get us into setting the target transform state will be setup.
        if (CurrentAppState != AppState.PickingAvatar)
        {
            CurrentAppState = AppState.WaitingForAnchor;
        }

        // Reset the underworld.
        if (UnderworldBase.Instance)
        {
            UnderworldBase.Instance.ResetUnderworld();
        }
    }

    void Update()
    {
        switch (CurrentAppState)
        {
            case AppState.PickingAvatar:
                // Avatar picking is done when the avatar picker has been dismissed.
                if (PlayerAvatarStore.Instance.PickerActive == false)
                {
                    CurrentAppState = AppState.WaitingForAnchor;
                }
                break;
            case AppState.WaitingForAnchor:
                // Once the anchor is established we need to run spatial mapping for a
                // little while to build up some meshes.
                if (ImportExportAnchorManager.Instance.AnchorEstablished)
                {
                    CurrentAppState = AppState.WaitingForStageTransform;
                    GestureManager.Instance.OverrideFocusedObject = HologramPlacement.Instance.gameObject;

                    SpatialMappingManager.Instance.gameObject.SetActive(true);
                    SpatialMappingManager.Instance.DrawVisualMeshes = true;
                    SpatialMappingDeformation.Instance.ResetGlobalRendering();
                    SpatialMappingManager.Instance.StartObserver();
                }
                break;
            case AppState.WaitingForStageTransform:
                // Now if we have the stage transform we are ready to go.
                if (HologramPlacement.Instance.GotTransform)
                {
                    CurrentAppState = AppState.Ready;
                    GestureManager.Instance.OverrideFocusedObject = shootHandler;
                }
                break;
        }
    }
}
  • En el panel Proyecto , vaya a la carpeta Scripts .
  • Haga doble clic en el script HologramPlacement para abrirlo en Visual Studio.
  • Reemplace el contenido por el código siguiente.
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the model.
    /// The model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    /// <summary>
    /// When the experience starts, we disable all of the rendering of the model.
    /// </summary>
    List<MeshRenderer> disabledRenderers = new List<MeshRenderer>();

    /// <summary>
    /// We use a voice command to enable moving the target.
    /// </summary>
    KeywordRecognizer keywordRecognizer;

    void Start()
    {
        // When we first start, we need to disable the model to avoid it obstructing the user picking a hat.
        DisableModel();

        // We care about getting updates for the model transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the model transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;

        // And if the users want to reset the stage transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.ResetStage] = this.OnResetStage;

        // Setup a keyword recognizer to enable resetting the target location.
        List<string> keywords = new List<string>();
        keywords.Add("Reset Target");
        keywordRecognizer = new KeywordRecognizer(keywords.ToArray());
        keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
        keywordRecognizer.Start();
    }

    /// <summary>
    /// When the keyword recognizer hears a command this will be called.  
    /// In this case we only have one keyword, which will re-enable moving the
    /// target.
    /// </summary>
    /// <param name="args">information to help route the voice command.</param>
    private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        ResetStage();
    }

    /// <summary>
    /// Resets the stage transform, so users can place the target again.
    /// </summary>
    public void ResetStage()
    {
        GotTransform = false;

        // AppStateManager needs to know about this so that
        // the right objects get input routed to them.
        AppStateManager.Instance.ResetStage();

        // Other devices in the experience need to know about this as well.
        CustomMessages.Instance.SendResetStage();

        // And we need to reset the object to its start animation state.
        GetComponent<EnergyHubBase>().ResetAnimation();
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the model if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    /// <summary>
    /// Turns off all renderers for the model.
    /// </summary>
    void DisableModel()
    {
        foreach (MeshRenderer renderer in gameObject.GetComponentsInChildren<MeshRenderer>())
        {
            if (renderer.enabled)
            {
                renderer.enabled = false;
                disabledRenderers.Add(renderer);
            }
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = false;
        }
    }

    /// <summary>
    /// Turns on all renderers that were disabled.
    /// </summary>
    void EnableModel()
    {
        foreach (MeshRenderer renderer in disabledRenderers)
        {
            renderer.enabled = true;
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = true;
        }

        disabledRenderers.Clear();
    }


    void Update()
    {
        // Wait till users pick an avatar to enable renderers.
        if (disabledRenderers.Count > 0)
        {
            if (!PlayerAvatarStore.Instance.PickerActive &&
            ImportExportAnchorManager.Instance.AnchorEstablished)
            {
                // After which we want to start rendering.
                EnableModel();

                // And if we've already been sent the relative transform, we will use it.
                if (GotTransform)
                {
                    // This triggers the animation sequence for the model and
                    // puts the cool materials on the model.
                    GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                }
            }
        }
        else if (GotTransform == false)
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        Vector3 retval;
        // We need to know how many users are in the experience with good transforms.
        Vector3 cumulatedPosition = Camera.main.transform.position;
        int playerCount = 1;
        foreach (RemotePlayerManager.RemoteHeadInfo remoteHead in RemotePlayerManager.Instance.remoteHeadInfos)
        {
            if (remoteHead.Anchored && remoteHead.Active)
            {
                playerCount++;
                cumulatedPosition += remoteHead.HeadObject.transform.position;
            }
        }

        // If we have more than one player ...
        if (playerCount > 1)
        {
            // Put the transform in between the players.
            retval = cumulatedPosition / playerCount;
            RaycastHit hitInfo;

            // And try to put the transform on a surface below the midpoint of the players.
            if (Physics.Raycast(retval, Vector3.down, out hitInfo, 5, SpatialMappingManager.Instance.LayerMask))
            {
                retval = hitInfo.point;
            }
        }
        // If we are the only player, have the model act as the 'cursor' ...
        else
        {
            // We prefer to put the model on a real world surface.
            RaycastHit hitInfo;

            if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hitInfo, 30, SpatialMappingManager.Instance.LayerMask))
            {
                retval = hitInfo.point;
            }
            else
            {
                // But if we don't have a ray that intersects the real world, just put the model 2m in
                // front of the user.
                retval = Camera.main.transform.position + Camera.main.transform.forward * 2;
            }
        }
        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the model to do its animation and
        // swap its materials.
        if (disabledRenderers.Count == 0 && GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    void OnResetStage(NetworkInMessage msg)
    {
        GotTransform = false;

        GetComponent<EnergyHubBase>().ResetAnimation();
        AppStateManager.Instance.ResetStage();
    }
}

Implementación y disfrute

  • Compile e implemente el proyecto en los dispositivos HoloLens.
  • Cuando la aplicación esté lista, fíjese en un círculo y observe cómo el EnergyHub aparece en el centro de todos.
  • Pulse para colocar EnergyHub.
  • Pruebe el comando de voz "Restablecer destino" para seleccionar la copia de seguridad de EnergyHub y trabajar juntos como un grupo para mover el holograma a una nueva ubicación.

Capítulo 6: Física Real-World

En este capítulo agregaremos hologramas que rebotan en superficies del mundo real. ¡Mira tu espacio lleno de proyectos lanzados por ti y tus amigos!

Objetivos

  • Lanza proyectiles que rebotan en superficies reales.
  • Comparte los proyectiles para que otros jugadores puedan verlos.

Instrucciones

  • En Jerarquía , seleccione el objeto HologramCollection .
  • En el inspector , haga clic en Agregar componente.
  • En el cuadro de búsqueda, escriba Projectile Launcher. Seleccione el resultado de la búsqueda.

Implementación y disfrute

  • Compile e implemente en los dispositivos HoloLens.
  • Cuando la aplicación se ejecute en todos los dispositivos, realice un toque de aire para iniciar el proyectil en superficies del mundo real.
  • ¡Mira lo que sucede cuando tu proyectil se colisiona con el avatar de otro jugador!

Capítulo 7: Gran final

En este capítulo, descubriremos un portal que solo se puede detectar con colaboración.

Objetivos

  • ¡Trabajen juntos para lanzar suficientes proyectiles en el delimitador para descubrir un portal secreto!

Instrucciones

  • En el panel Proyecto , vaya a la carpeta Hologramas .
  • Arrastre y coloque el recurso Underworld como elemento secundario de HologramCollection.
  • Con HologramCollection seleccionado, haga clic en el botón Agregar componente del Inspector.
  • En el menú, escriba en el cuadro de búsqueda ExplodeTarget. Seleccione el resultado de la búsqueda.
  • Con HologramCollection seleccionado, desde hierarchy arrastre el objeto EnergyHub al campo Target del Inspector.
  • Con HologramCollection seleccionado, desde hierarchy arrastre el objeto Underworld al campo Underworld del Inspector.

Implementación y disfrute

  • Compile e implemente en los dispositivos HoloLens.
  • Cuando se haya iniciado la aplicación, colabore juntos para iniciar los proyectiles en EnergyHub.
  • Cuando aparezca el inframundo, lanza proyectiles en robots del inframundo (golpea a un robot tres veces para divertirte más).