HoloLens (1ª gen) Partilha 240: Múltiplos dispositivos de HoloLens

Importante

Os tutoriais da Mixed Reality Academy foram desenhados com HoloLens (1ª gen), Unidade 2017 e Auscultadores Imersivos de Realidade Mista em mente. Como tal, sentimos que é importante deixar estes tutoriais no lugar para os desenvolvedores que ainda estão à procura de orientação no desenvolvimento para esses dispositivos. Estes tutoriais não serão atualizados com os mais recentes instrumentos ou interações que estão a ser utilizados para HoloLens 2 e podem não ser compatíveis com versões mais recentes da Unidade. Serão mantidos para continuar a trabalhar nos dispositivos suportados. Uma nova série de tutoriais foi publicada para HoloLens 2.

Hologramas são dadas presença no nosso mundo mantendo-se no lugar à medida que nos movemos no espaço. HoloLens mantém os hologramas no lugar utilizando vários sistemas de coordenadas para acompanhar a localização e orientação dos objetos. Quando partilhamos estes sistemas de coordenadas entre dispositivos, podemos criar uma experiência partilhada que nos permita participar num mundo holográfico partilhado.

Neste tutorial, iremos:

  • Configurar uma rede para uma experiência partilhada.
  • Partilhe hologramas em dispositivos HoloLens.
  • Descubra outras pessoas no nosso mundo holográfico partilhado.
  • Crie uma experiência interativa partilhada onde pode direcionar outros jogadores - e lançar projéteis neles!

Suporte de dispositivos

Curso HoloLens Auscultadores imersivos
MR Sharing 240: Múltiplos dispositivos de HoloLens ✔️

Antes de começar

Pré-requisitos

Project ficheiros

  • Descarregue os ficheiros exigidos pelo projeto. Requer Unidade 2017.2 ou mais tarde.
    • Se ainda precisar de suporte da Unidade 5.6, utilize esta versão.
    • Se ainda precisar de suporte da Unidade 5.5, utilize esta versão.
    • Se ainda precisar de suporte unidade 5.4, utilize esta versão.
  • Desaprove os ficheiros para o seu ambiente de trabalho ou outro local de fácil acesso. Mantenha o nome da pasta como SharedHolograms.

Nota

Se quiser ver o código fonte antes de descarregar, está disponível no GitHub.

Capítulo 1 - Holo World

Neste capítulo, vamos configurar o nosso primeiro projeto de Unidade e passar pelo processo de construção e implantação.

Objetivos

  • Unidade de Configuração para desenvolver aplicações holográficas.
  • Veja o seu holograma!

Instruções

  • Iniciar a Unidade.
  • Selecione Abrir.
  • Introduza o local como a pasta SharedHolograms que você anteriormente não foi aqueved.
  • Selecione Project Nome e clique em Selecionar Pasta.
  • Na Hierarquia,clique à direita na Câmara Principal e selecione Delete.
  • Na pasta HoloToolkit-Sharing-240/Prefabs/Camera, encontre o pré-fabricado da Câmara Principal.
  • Arraste e deixe cair a Câmara Principal na Hierarquia.
  • Na Hierarquia,clique em Criar e Criar Vazio.
  • Clique com o botão direito no novo GameObject e selecione Rename.
  • Mude o nome do GameObject para HologramCollection.
  • Selecione o objeto HologramCollection na Hierarquia.
  • No Inspetor definir a posição de transformação para: X: 0, Y: -0,25, Z: 2.
  • Na pasta Hologramas no painel Project,encontre o ativo EnergyHub.
  • Arraste e largue o objeto EnergyHub do painel Project para a Hierarquia como uma criança de HologramCollection.
  • Selecione arquivo salvar cena como...
  • Nomeie a cena SharedHolograms e clique em Guardar.
  • Prima o botão Reproduzir unidade para visualizar os hologramas.
  • Prima Reproduzir uma segunda vez para parar o modo de pré-visualização.

Exportar o projeto de Unidade para Visual Studio

  • In Unitity selecione File Build Definições.
  • Clique em Adicionar Cenas Abertas para adicionar a cena.
  • Selecione Plataforma de Windows Universal na lista de plataformas e clique na Plataforma Switch.
  • Desa estava o SDK a Universal 10.
  • Definir o dispositivo-alvo para HoloLens e UWP Build Type para D3D.
  • Verifique projetos de unidade C#.
  • Clique em Construir.
  • Na janela do explorador de ficheiros que aparece, crie uma Nova Pasta chamada "App".
  • Clique na pasta App.
  • Prima Selecionar Pasta.
  • Quando a unidade estiver terminada, aparecerá uma janela do Explorador de Ficheiros.
  • Abra a pasta App.
  • Open SharedHolograms.sln para lançar Visual Studio.
  • Utilizando a barra de ferramentas superior em Visual Studio, altere o alvo de Debug para Release e de ARM para X86.
  • Clique na seta para baixo ao lado da Máquina Local e selecione Dispositivo Remoto.
    • Desaponhe o Endereço no nome ou endereço IP do seu HoloLens. Se não conhece o endereço IP do seu dispositivo, procure Definições Network Internet Advanced &> Options ou pergunte-Cortana &
    • Deixe o modo de autenticação definido para a Universal.
    • Clique em Selecionar
  • Clique em Debug Start Sem depurar ou prima Ctrl + F5. Se esta for a primeira vez que se implanta no seu dispositivo, terá de emparelhá-lo com Visual Studio.
  • Ponha o HoloLens e encontre o holograma EnergyHub.

Capítulo 2 - Interação

Neste capítulo, vamos interagir com os nossos hologramas. Primeiro, vamos adicionar um cursor para visualizar o nosso Gaze. Depois, adicionamos Gestos e usamos a mão para colocar os nossos hologramas no espaço.

Objetivos

  • Utilize a entrada de olhar para controlar um cursor.
  • Use a entrada de gestos para interagir com hologramas.

Instruções

Olhar

  • No painel hierarquia selecione o objeto HologramCollection.
  • No painel do Inspetor clique no botão Adicionar Componente.
  • No menu, digite na caixa de pesquisa Gaze Manager. Selecione o resultado da pesquisa.
  • Na pasta HoloToolkit-Sharing-240\Prefabs\Input, encontre o ativo Cursor.
  • Arraste e deixe cair o ativo cursor para a Hierarquia.

Gesto

  • No painel hierarquia selecione o objeto HologramCollection.
  • Clique em Adicionar Componente e digite Gestor de Gestos no campo de pesquisa. Selecione o resultado da pesquisa.
  • No painel hierárquico,expanda a HologramCollection.
  • Selecione o objeto EnergyHub da criança.
  • No painel do Inspetor clique no botão Adicionar Componente.
  • No menu, digite na caixa de pesquisa Hologram Placement. Selecione o resultado da pesquisa.
  • Guarde a cena selecionando a cena de salvamento de ficheiros.

Implementar e desfrutar

  • Construa e desloque-se para o seu HoloLens, utilizando as instruções do capítulo anterior.
  • Assim que a aplicação for lançada no seu HoloLens, mova a cabeça e perceba como o EnergyHub segue o seu olhar.
  • Note como o cursor aparece quando olha para o holograma e muda para uma luz de ponto quando não olha para um holograma.
  • Faça uma torneira de ar para colocar o holograma. Neste momento do nosso projeto, só é possível colocar o holograma uma vez (reimplantar para tentar novamente).

Capítulo 3 - Coordenadas Partilhadas

É divertido ver e interagir com hologramas, mas vamos mais longe. Vamos criar a nossa primeira experiência partilhada- um holograma que todos podem ver juntos.

Objetivos

  • Configurar uma rede para uma experiência partilhada.
  • Estabelecer um ponto de referência comum.
  • Partilhe sistemas de coordenadas através de dispositivos.
  • Todos vêem o mesmo holograma!

Nota

As capacidades do InternetClientServer e privateNetworkClientServer devem ser declaradas para uma aplicação para se ligar ao servidor de partilha. Isto já é feito para si já em Hologramas 240, mas lembre-se disso para os seus próprios projetos.

  1. No Editor de Unidade, vá às definições do jogador navegando para "Editar > Project Definições > Player"
  2. Clique no separador "Windows Store"
  3. Na secção "Publicar > capacidades Definições", verifique a capacidade do > e a capacidade privateNetworkClientServer

Instruções

  • No painel Project navegue para a pasta HoloToolkit-Sharing-240\Prefabs\Sharing.
  • Arraste e deixe cair o pré-fabricado de partilha no painel da Hierarquia.

Em seguida, precisamos lançar o serviço de partilha. Apenas um PC na experiência partilhada precisa de fazer este passo.

  • In Unitity - no menu de primeira mão - selecione o menu HoloToolkit-Sharing-240.
  • Selecione o item serviço de partilha de lançamento no drop-down.
  • Verifique a opção Rede Privada e clique em Permitir o Acesso quando aparecer o pedido de firewall.
  • Note o endereço IPv4 apresentado na janela da consola 'Serviço de Partilha'. Este é o mesmo IP que a máquina em que o serviço está a ser executado.

Siga o resto das instruções em todos os PCs que se juntarão à experiência partilhada.

  • Na Hierarquia,selecione o objeto Partilhar.
  • No Inspetor, no componente De Partilha, altere o Endereço do Servidor de 'localhost' para o endereço IPv4 da máquina em funcionamento SharingService.exe.
  • Na Hierarquia selecione o objeto HologramCollection.
  • No Inspetor clique no botão Adicionar Componente.
  • Na caixa de pesquisa, escreva Import Export Anchor Manager. Selecione o resultado da pesquisa.
  • No painel Project navegue para a pasta Scripts.
  • Clique duas vezes no script HologramPlacement para o abrir em Visual Studio.
  • Substitua o conteúdo pelo código abaixo.
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.
    }
}
  • De volta à Unidade, selecione a HologramCollection no painel da Hierarquia.
  • No painel do Inspetor clique no botão Adicionar Componente.
  • No menu, digite na caixa de pesquisa App State Manager. Selecione o resultado da pesquisa.

Implementar e desfrutar

  • Construa o projeto para os seus dispositivos HoloLens.
  • Designe um HoloLens para ser lançado primeiro. Terá de esperar que a Âncora seja carregada para o serviço antes de poder colocar o EnergyHub (isto pode demorar ~30-60 segundos). Até que o upload esteja feito, os seus gestos de sapateado serão ignorados.
  • Depois de o EnergyHub ter sido colocado, a sua localização será enviada para o serviço e poderá ser implantada em todos os outros dispositivos HoloLens.
  • Quando um novo HoloLens se junta pela primeira vez à sessão, a localização do EnergyHub pode não estar correta nesse dispositivo. No entanto, assim que as localizações da âncora e do EnergyHub forem descarregadas do serviço, o EnergyHub deverá saltar para o novo local partilhado. Se isso não acontecer dentro de ~30-60 segundos, caminhe até onde estava a HoloLens original ao definir a âncora para recolher mais pistas ambientais. Se a localização ainda não se travar, reposicione-se no dispositivo.
  • Quando os dispositivos estiverem prontos e executarem a aplicação, procure o EnergyHub. Podem todos concordar com a localização do holograma e em que direção o texto está virado?

Capítulo 4 - Descoberta

Agora todos podem ver o mesmo holograma! Agora vamos ver todos os outros ligados ao nosso mundo holográfico partilhado. Neste capítulo, vamos agarrar a localização da cabeça e a rotação de todos os outros dispositivos HoloLens na mesma sessão de partilha.

Objetivos

  • Descubram-se na nossa experiência partilhada.
  • Escolha e partilhe um avatar de jogador.
  • Prenda o avatar do jogador ao lado das cabeças de todos.

Instruções

  • No painel Project navegue para a pasta Hologramas.
  • Arraste e deixe cair o PlayerAvatarStore na Hierarquia.
  • No painel Project navegue para a pasta Scripts.
  • Clique duas vezes no script AvatarSelector para abri-lo em Visual Studio.
  • Substitua o conteúdo pelo código abaixo.
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;
    }
}
  • Na Hierarquia selecione o objeto HologramCollection.
  • No Inspetor clique em Adicionar Componente.
  • Na caixa de pesquisa, digite Gerente de Jogador Local. Selecione o resultado da pesquisa.
  • Na Hierarquia selecione o objeto HologramCollection.
  • No Inspetor clique em Adicionar Componente.
  • Na caixa de pesquisa, digite Remote Player Manager. Selecione o resultado da pesquisa.
  • Abra o script HologramPlacement em Visual Studio.
  • Substitua o conteúdo pelo código abaixo.
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 o script AppStateManager em Visual Studio.
  • Substitua o conteúdo pelo código abaixo.
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;
        }
    }
}

Implementar e desfrutar

  • Construa e implemente o projeto para os seus dispositivos HoloLens.
  • Quando ouvir um som de pinging, encontre o menu de seleção do avatar e selecione um avatar com o gesto de toque de ar.
  • Se não estiver a olhar para nenhum holograma, a luz de ponto em torno do seu cursor mudará uma cor diferente quando o seu HoloLens estiver a comunicar com o serviço: inicialização (púrpura escura), download da âncora (verde), importação/exportação de dados de localização (amarelo), upload da âncora (azul). Se a sua luz de ponto em torno do seu cursor for a cor padrão (roxo claro), então está pronto para interagir com outros jogadores na sua sessão!
  • Olhe para outras pessoas ligadas ao seu espaço - haverá um robô holográfico flutuando acima do ombro e imitando os seus movimentos de cabeça!

Capítulo 5 - Colocação

Neste capítulo, faremos com que a âncora seja colocada em superfícies do mundo real. Usaremos coordenadas partilhadas para colocar a âncora no ponto médio entre todos ligados à experiência partilhada.

Objetivos

  • Coloque hologramas na malha de mapeamento espacial com base na posição da cabeça dos jogadores.

Instruções

  • No painel Project navegue para a pasta Hologramas.
  • Arraste e deixe cair o pré-fabricado CustomSpatialMapping para a Hierarquia.
  • No painel Project navegue para a pasta Scripts.
  • Clique duas vezes no script AppStateManager para o abrir em Visual Studio.
  • Substitua o conteúdo pelo código abaixo.
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;
        }
    }
}
  • No painel Project navegue para a pasta Scripts.
  • Clique duas vezes no script HologramPlacement para o abrir em Visual Studio.
  • Substitua o conteúdo pelo código abaixo.
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();
    }
}

Implementar e desfrutar

  • Construa e implemente o projeto para os seus dispositivos HoloLens.
  • Quando a aplicação estiver pronta, fique em círculo e note como o EnergyHub aparece no centro de todos.
  • Toque para colocar o EnergyHub.
  • Experimente o comando de voz 'Reset Target' para recolher o EnergyHub e trabalhar em conjunto como um grupo para mover o holograma para um novo local.

Capítulo 6 - Física Real-World

Neste capítulo vamos adicionar hologramas que saltam das superfícies do mundo real. Veja o seu espaço repleto de projetos lançados por si e pelos seus amigos!

Objetivos

  • Lançar projéteis que saltam das superfícies do mundo real.
  • Partilhe os projéteis para que outros jogadores possam vê-los.

Instruções

  • Na Hierarquia selecione o objeto HologramCollection.
  • No Inspetor clique em Adicionar Componente.
  • Na caixa de pesquisa, escreva o Projectile Launcher. Selecione o resultado da pesquisa.

Implementar e desfrutar

  • Construa e implemente os seus dispositivos HoloLens.
  • Quando a aplicação estiver em funcionamento em todos os dispositivos, execute um air-tap para lançar projétil em superfícies reais.
  • Veja o que acontece quando o projétil colide com o avatar de outro jogador!

Capítulo 7 - Grande Final

Neste capítulo, vamos descobrir um portal que só pode ser descoberto com colaboração.

Objetivos

  • Trabalhem juntos para lançar projéteis suficientes na âncora para descobrir um portal secreto!

Instruções

  • No painel Project navegue para a pasta Hologramas.
  • Arraste e largue o ativo do Submundo como uma criança do HologramCollection.
  • Com o HologramCollection selecionado, clique no botão Adicionar Componente no Inspetor.
  • No menu, digite na caixa de pesquisa ExplodeTarget. Selecione o resultado da pesquisa.
  • Com o HologramCollection selecionado, da Hierarquia arrastam o objeto EnergyHub para o campo Alvo no Inspetor.
  • Com o HologramCollection selecionado, da Hierarquia arrastam o objeto do Submundo para o campo do Submundo no Inspetor.

Implementar e desfrutar

  • Construa e implemente os seus dispositivos HoloLens.
  • Quando a aplicação tiver sido lançada, colabore em conjunto para lançar projéteis no EnergyHub.
  • Quando o submundo aparecer, lance projéteis em robôs do submundo (bata num robô três vezes por diversão extra).