Spawn a Player using AR Foundation

Code and workflows for using AR Foundation to spawn an AR Player

What you'll develop on this page

Your project will be able to make use of AR Player input configurations when deployed on ARKit enabled platforms.

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Updating-to-AR-Player

Updating controls

Sampling AR pose

Almost all AR APIs are able to provide the device's "pose" (translation + rotation). AR Foundation provides this data through its "Pose Driver."

AR Pose Driver

The AR Pose Driver drives the local position and orientation of the parent GameObject according to the device's tracking information. The most common use case for this would be attaching the ARPoseDriver to the AR Camera to drive the camera's position and orientation in an AR scene.

From AR Foundation's AR Pose Driver documentation

In order to spawn an AR Player in our game, we need to grab AR pose data from AR Foundation and provide it to our ECS system. AR Foundation runs on MonoBehaviours, so we're going to need to use a pattern of dropping data "into" ECS (using the Entity Manager), and then pulling it back "out" when we need it. Currently, ECS does not provide a way to "push" the information out (i.e. by calling a MonoBehaviour method within a system).

The device's movement will control our AR player's movement. So if the device moves to the left, the player moves to the left. If the device moves to the right, the player moves to the right. On the client, we will sample the pose and use NetCode to send it to the server as ICommandData.

  • Let's create ARPoseComponent in the Client/Components folder

  • Paste the code snippet below into ARPoseComponent.cs:

using Unity.Entities;
using Unity.Collections;
using Unity.Transforms;

public struct ARPoseComponent : IComponentData
{
    public Translation translation;
    public Rotation rotation;
}

This is going to be the component that will update every time we sample pose data.

Now we need to actually create the MonoBehaviour that will grab the pose data, and then set ARPoseComponent. From the Unity AR Foundation documentation above, we know that the AR Pose Driver script updates the translation and rotation of the parent GameObject. We are going to add AR Pose Sampler to the same GameObject and pull its transformation and rotation.

  • In MainScene, select the AR Camera that is nested in AR Session Origin, click "Add Component" in Inspector and create a new script called ARPoseSampler, and move the script to the Client folder

  • Paste the code snippet below into ARPoseSampler.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.NetCode;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine.XR.ARFoundation;
using Unity.Mathematics;

public class ARPoseSampler : MonoBehaviour
{
    //We will be using ClientSimulationSystemGroup to update our ARPoseComponent
    private ClientSimulationSystemGroup m_ClientSimGroup;
    
    void Start()
    {
        //We grab ClientSimulationSystemGroup to update ARPoseComponent in our Update loop
        foreach (var world in World.All)
        {
            if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
            {
                //We create the ARPoseComponent that we will update with new data
                world.EntityManager.CreateEntity(typeof(ARPoseComponent));
                //We grab the ClientSimulationSystemGroup for our Update loop
                m_ClientSimGroup = world.GetExistingSystem<ClientSimulationSystemGroup>();
            }
        }        
    }

    // Update is called once per frame
    void Update()
    {
        //We create a new Translation and Rotation from the transform of the GameObject
        //The GameObject Translation and Rotation is updated by the pose driver
        var arTranslation = new Translation {Value = transform.position};
        var arRotation = new Rotation {Value = transform.rotation};
        //Now we update our ARPoseComponent with the updated Pose Driver data
        var arPose = new ARPoseComponent {
           translation = arTranslation,
           rotation = arRotation 
        };
        m_ClientSimGroup.SetSingleton<ARPoseComponent>(arPose);
    }
}

Updating PlayerCommand

We are going to use PlayerCommand to send the pose data, so we'll need to update the script in order to do so.

We are also going to need to add a boolean to PlayerCommand to signify that the command was sent from an AR player, now that the server will need to differentiate between AR and desktop players.

  • Paste the code snippet below into PlayerCommand.cs:

using Unity.Networking.Transport;
using Unity.NetCode;
using Unity.Burst;
using Unity.Entities;
using Unity. Transforms;
using Unity.Mathematics;


[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
public struct PlayerCommand : ICommandData
{
    public uint Tick {get; set;}
    public byte right;
    public byte left;
    public byte thrust;
    public byte reverseThrust;
    public byte selfDestruct;
    public byte shoot;
    public float mouseX;
    public float mouseY;
    public byte isAR;
    public float3 arTranslation;
    public quaternion arRotation;
}

Now PlayerCommand is able to grab the pose and send the pose to the server.

Create ARInputSystem

Rather than adding if(ar) statements into InputSystem we are going to create a new input system in Client/Systems just for AR players, named "ARInputSystem."

  • Create ARInputSystem in the Client/Systems folder

  • Paste the code snippet below into ARInputSystem.cs:

using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.NetCode;
using Unity.Jobs;
using Unity.Collections;
using UnityEngine.XR;
using Unity.Physics;

[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class ARInputSystem : SystemBase
{
    //We will need a command buffer for structural changes
    private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
    //We will grab the ClientSimulationSystemGroup because we require its tick in the ICommandData
    private ClientSimulationSystemGroup m_ClientSimulationSystemGroup;

    protected override void OnCreate()
    {
        //We set our variables
        m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
        m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
        //We will only run this system if the player is in game and if the palyer is an AR player
        RequireSingletonForUpdate<NetworkStreamInGame>();
        RequireSingletonForUpdate<IsARPlayerComponent>();
    }

    protected override void OnUpdate()
    {
        //The only inputs is for shooting or for self destruction
        //Movement will be through the ARPoseComponent
        byte selfDestruct, shoot;
        selfDestruct = shoot = 0;

        //More than 2 touches will register as self-destruct
        if (Input.touchCount > 2)
        {
            selfDestruct = 1;
        }
        //A single touch will register as shoot
        if (Input.touchCount == 1)
        {
            shoot = 1;
        }

        //We grab the AR pose to send to the server for movement
        var arPoseDriver = GetSingleton<ARPoseComponent>();
        //We must declare our local variables before the .ForEach()
        var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
        var inputFromEntity = GetBufferFromEntity<PlayerCommand>();
        var inputTargetTick = m_ClientSimulationSystemGroup.ServerTick;

        TryGetSingletonEntity<PlayerCommand>(out var targetEntity);
        Job.WithCode(() => {
        if (targetEntity == Entity.Null)
        {
            if (shoot != 0)
            {
                var req = commandBuffer.CreateEntity();
                commandBuffer.AddComponent<PlayerSpawnRequestRpc>(req);
                commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent());
            }
        }
        else
        {
            var input = inputFromEntity[targetEntity];
            input.AddCommandData(new PlayerCommand{Tick = inputTargetTick,
            selfDestruct = selfDestruct, shoot = shoot,
            isAR = 1,
            arTranslation = arPoseDriver.translation.Value,
            arRotation = arPoseDriver.rotation.Value});

        }
        }).Schedule();
        
        //We need to add the jobs dependency to the command buffer
        m_BeginSimEcb.AddJobHandleForProducer(Dependency);
    }
}

Update response system

Next, we need to update one of our response systems. We will only need to update the InputResponseMovementSystem (and not InputResponseSpawnSystem) because the spawning of bullets does not need to be altered. If the PlayerCommand has shoot = 1, then the bullet will spawn.

  • Paste the code snippet below into InputResponseMovementSystem.cs:

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.NetCode;
using Unity.Networking.Transport.Utilities;
using Unity.Collections;
using Unity.Physics;
using Unity.Jobs;
using UnityEngine;

//InputResponseMovementSystem runs on both the Client and Server
//It is predicted on the client but "decided" on the server
[UpdateInWorld(TargetWorld.ClientAndServer)] 
public partial class InputResponseMovementSystem : SystemBase
{
    //This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
    private GhostPredictionSystemGroup m_PredictionGroup;
    

    protected override void OnCreate()
    {
        // m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();

        //We will grab this system so we can use its "prediction tick" and "DeltaTime"
        m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
        
    }

    protected override void OnUpdate()
    {
        //No need for a CommandBuffer because we are not making any structural changes to any entities
        //We are setting values on components that already exist
        // var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();

        //These are special NetCode values needed to work the prediction system
        var currentTick = m_PredictionGroup.PredictingTick;
        var deltaTime = m_PredictionGroup.Time.DeltaTime;

        //We must declare our local variables before the .ForEach()
        var playerForce = GetSingleton<GameSettingsComponent>().playerForce;

        //We will grab the buffer of player commands from the player entity
        var inputFromEntity = GetBufferFromEntity<PlayerCommand>(true);

        //We are looking for player entities that have PlayerCommands in their buffer
        Entities
        .WithReadOnly(inputFromEntity)
        .WithAll<PlayerTag, PlayerCommand>()
        .ForEach((Entity entity, ref Translation translation, ref Rotation rotation, ref PhysicsVelocity velocity,
                in GhostOwnerComponent ghostOwner, in PredictedGhostComponent prediction) =>
        {
            //Here we check if we SHOULD do the prediction based on the tick, if we shouldn't, we return
            if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
                return;

            //We grab the buffer of commands from the player entity
            var input = inputFromEntity[entity];

            //We then grab the Command from the current tick (which is the PredictingTick)
            //if we cannot get it at the current tick we make sure shoot is 0
            //This is where we will store the current tick data
            PlayerCommand inputData;
            if (!input.GetDataAtTick(currentTick, out inputData))
                inputData.shoot = 0;

            if (inputData.right == 1)
            {   //thrust to the right of where the player is facing
                velocity.Linear += math.mul(rotation.Value, new float3(1,0,0)).xyz * playerForce * deltaTime;                
            }
            if (inputData.left == 1)
            {   //thrust to the left of where the player is facing
                velocity.Linear += math.mul(rotation.Value, new float3(-1,0,0)).xyz * playerForce * deltaTime;
            }
            if (inputData.thrust == 1)
            {   //thrust forward of where the player is facing
                velocity.Linear += math.mul(rotation.Value, new float3(0,0,1)).xyz * playerForce * deltaTime;
            }
            if (inputData.reverseThrust == 1)
            {   //thrust backwards of where the player is facing
                velocity.Linear += math.mul(rotation.Value, new float3(0,0,-1)).xyz * playerForce * deltaTime;
            }

            
            if (inputData.mouseX != 0 || inputData.mouseY != 0)
            {   //move the mouse
                //here we have "hardwired" the look speed, we could have included this in the GameSettingsComponent to make it configurable
                float lookSpeedH = 2f;
                float lookSpeedV = 2f;
                Quaternion currentQuaternion = rotation.Value; 
                float yaw = currentQuaternion.eulerAngles.y;
                float pitch = currentQuaternion.eulerAngles.x;

                //MOVING WITH MOUSE
                yaw += lookSpeedH * inputData.mouseX;
                pitch -= lookSpeedV * inputData.mouseY;
                Quaternion newQuaternion = Quaternion.identity;
                newQuaternion.eulerAngles = new Vector3(pitch,yaw, 0);
                rotation.Value = newQuaternion;
            }
            //If the PlayerCommand is from an AR player we will update movement in a special way
            if (inputData.isAR == 1)
            {
                //The player is going to be (0,-2,10) relative to the AR pose
                //This will make the player appear a bit lower and in front of the camera, making it easier to control
                translation.Value = (inputData.arTranslation) - (math.mul(rotation.Value, new float3(0,2,0)).xyz) + (math.mul(rotation.Value, new float3(0,0,10)).xyz);
                //The player will face the same direction as the camera
                rotation.Value = (inputData.arRotation);
            }

        }).ScheduleParallel();

        //No need to .AddJobHandleForProducer() because we did not need a CommandBuffer to make structural changes
    }   
}

In InputResponseMovementSystem, we hardcoded the offset of our camera to our player as (0, 2, -10). We instead could've included this offset as part of our GameSettingsComponent, but we decided to leave it hardcoded for the sake of simplicity.

Update spawning classification for AR players

Now we need to update our PlayerGhostSpawnClassificationSystem so it does not add the camera to an AR Player (remember that in PlayerGhostSpawnClassificationSystem we add a Camera to the player after spawning).

We are going to check for the IsARPlayerComponent singleton and if it exists we will not add the Camera to the player.

  • Paste the code snippet below into PlayerGhostSpawnClassificationSystem.cs:

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.NetCode;
using UnityEngine;
using Unity.Transforms;
using Unity.Mathematics;

//We are updating only in the client world because only the client must specify exactly which player entity it "owns"
[UpdateInWorld(TargetWorld.Client)]
//We will be updating after NetCode's GhostSpawnClassificationSystem because we want
//to ensure that the PredictedGhostComponent (which it adds) is available on the player entity to identify it
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
public partial class PlayerGhostSpawnClassificationSystem : SystemBase
{
    private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;

    //We will store the Camera prefab here which we will attach when we identify our player entity
    private Entity m_CameraPrefab;

    protected override void OnCreate()
    {
        m_BeginSimEcb = World.GetExistingSystem<BeginSimulationEntityCommandBufferSystem>();

        //We need to make sure we have NCE before we start the update loop (otherwise it's unnecessary)
        RequireSingletonForUpdate<NetworkIdComponent>();
        RequireSingletonForUpdate<CameraAuthoringComponent>();
    }

    protected override void OnUpdate()
    {
        //Here we set the prefab we will use
        if (m_CameraPrefab == Entity.Null)
        {
            //We grab our camera and set our variable
            m_CameraPrefab = GetSingleton<CameraAuthoringComponent>().Prefab;
            return;
        }
        
        var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
        
        //We must declare our local variables before using them
        var camera = m_CameraPrefab;
        //The "playerEntity" is the NCE
        var networkIdComponent = GetSingleton<NetworkIdComponent>();
        //The false is to signify that the data will NOT be read-only
        var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
        //Check if this is an AR player
        IsARPlayerComponent arComponent;
        var isAR = TryGetSingleton<IsARPlayerComponent>(out arComponent);

        //We will look for Player prefabs that we have not added a "PlayerClassifiedTag" to (which means we have checked the player if it is "ours")
        Entities
        .WithAll<PlayerTag>()
        .WithNone<PlayerClassifiedTag>()
        .ForEach((Entity entity, int entityInQueryIndex, in GhostOwnerComponent ghostOwnerComponent) =>
        {
            // If this is true this means this Player is mine (because the GhostOwnerComponent value is equal to the NetworkId)
            // Remember the GhostOwnerComponent value is set by the server and is ghosted to the client
            if (ghostOwnerComponent.NetworkId == networkIdComponent.Value)
            {
                if(!isAR)
                {
                    //This creates our camera
                    var cameraEntity = commandBuffer.Instantiate(entityInQueryIndex, camera);
                    //This is how you "attach" a prefab entity to another
                    commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new Parent { Value = entity });
                    commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new LocalToParent() );
                }
            }
            // This means we have classified this Player prefab
            commandBuffer.AddComponent(entityInQueryIndex, entity, new PlayerClassifiedTag() );

        }).ScheduleParallel();

        m_BeginSimEcb.AddJobHandleForProducer(Dependency);
    }
}
  • Let's hit play, host a game, and spawn a player

If you remember back to our "DOTS Entity Component System ECS" section, we added the following to our "Scripting Define Symbols": HYBRID_ENTITIES_CAMERA_CONVERSION. Now that we are building for iOS, we also need to add this to our iOS Player Settings. This way when we hit "play" in the Unity editor we are still able to render our camera (instead of constantly switching back and forth between Desktop and iOS build).

  • Go to "Player Settings..." (File > Build Settings > Player Settings button in the bottom left) and add HYBRID_ENTITIES_CAMERA_CONVERSION to the "Scripting Define Symbols" field when Player is selected, after "UNITY_XR_ARKIT_LOADER_ENABLED;"

    • Hit apply

  • Save!

  • Ok, now you need to restart your computer (yup, we're being serious; in our testing, this was a necessary step in order for the camera to work in the editor again)

  • Go to the BuildSettings folder, choose your development platform, and hit "Build and Run" (this will cause the Editor to switch)

    • The top of the Editor will change from "iOS" to "PC, Mac & Linux"

  • Hit play, host a game, and spawn a player

  • Now let's try something different. Unclick play, go back to BuildSettings folder, select iOS-Build and hit Build and Run (this will cause editor to switch)

  • Once the Editor resets to "iOS" hit play, host a game, and spawn a player

  • Now click "Build and Run" again to build to Xcode

  • In the deployed app, host a game

  • Tap on the screen to spawn and shoot, use 3 fingers to self-destruct, and move the device to move the player

We are now able to control our AR player

  • We created ARPoseSampler

  • We updated PlayerCommand

  • We updated InputResponseMovementSystem

  • We updated PlayerGhostSpawnClassificationSystem

Updating spawning

Currently, InputMovementResponseSystem places the player ahead of the AR pose, regardless of where our server spawns our AR player. This isn't great because it means as soon as a player is destroyed (via self-destructing or via another player shooting at them), it will regenerate right where they were, which is not very fun.

We need to update our flow so that when our AR Player is spawned we can tell our AR Pose Driver to move the camera to behind spawn location. We need to communicate to Pose Driver and say "you are now at translation + rotation" position.

A good place to do this is PlayerGhostSpawnClassificationSystem. When we receive our player ghost on the client, we will create a new singleton called "SpawnPositionForARComponent" which will be the location our server spawned our player.

Within ARPoseSampler we will add logic to check for this component, and if it exists (which means there was a new spawn), we will update the pose to be behind the player and then delete the singleton.

  • Create SpawnPositionForARComponent in the Client/Components folder

  • Paste the code snippet below into SpawnPositionForARComponent.cs:

using Unity.Entities;
using UnityEngine;
using Unity.Mathematics;

public struct SpawnPositionForARComponent : IComponentData
{
    public float3 spawnTranslation;
    public quaternion spawnRotation;
}
  • Now we need to update PlayerGhostSpawnClassificationSystem in order to create a singleton with the spawn position

  • Paste the code snippet below into PlayerGhostSpawnClassificationSystem.cs:

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.NetCode;
using UnityEngine;
using Unity.Transforms;
using Unity.Mathematics;

//We are updating only in the client world because only the client must specify exactly which player entity it "owns"
[UpdateInWorld(TargetWorld.Client)]
//We will be updating after NetCode's GhostSpawnClassificationSystem because we want
//to ensure that the PredictedGhostComponent (which it adds) is available on the player entity to identify it
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
public partial class PlayerGhostSpawnClassificationSystem : SystemBase
{
    private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;

    //We will store the Camera prefab here which we will attach when we identify our player entity
    private Entity m_CameraPrefab;

    protected override void OnCreate()
    {
        m_BeginSimEcb = World.GetExistingSystem<BeginSimulationEntityCommandBufferSystem>();

        //We need to make sure we have NCE before we start the update loop (otherwise it's unnecessary)
        RequireSingletonForUpdate<NetworkIdComponent>();
        RequireSingletonForUpdate<CameraAuthoringComponent>();
    }

    protected override void OnUpdate()
    {
        //Here we set the prefab we will use
        if (m_CameraPrefab == Entity.Null)
        {
            //We grab our camera and set our variable
            m_CameraPrefab = GetSingleton<CameraAuthoringComponent>().Prefab;
            return;
        }
        
        var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
        
        //We must declare our local variables before using them
        var camera = m_CameraPrefab;
        //The "playerEntity" is the NCE
        var networkIdComponent = GetSingleton<NetworkIdComponent>();
        //The false is to signify that the data will NOT be read-only
        var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
        //Check if this is an AR player
        IsARPlayerComponent arComponent;
        var isAR = TryGetSingleton<IsARPlayerComponent>(out arComponent);

        //We will look for Player prefabs that we have not added a "PlayerClassifiedTag" to (which means we have checked the player if it is "ours")
        Entities
        .WithAll<PlayerTag>()
        .WithNone<PlayerClassifiedTag>()
        .ForEach((Entity entity, int entityInQueryIndex, in GhostOwnerComponent ghostOwnerComponent, in Translation translation, in Rotation rotation) =>
        {
            // If this is true this means this Player is mine (because the GhostOwnerComponent value is equal to the NetworkId)
            // Remember the GhostOwnerComponent value is set by the server and is ghosted to the client
            if (ghostOwnerComponent.NetworkId == networkIdComponent.Value)
            {
                if(!isAR)
                {
                    //This creates our camera
                    var cameraEntity = commandBuffer.Instantiate(entityInQueryIndex, camera);
                    //This is how you "attach" a prefab entity to another
                    commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new Parent { Value = entity });
                    commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new LocalToParent() );
                }
                //If we are an AR player we will create SpawnPositionForARoComponent
                if (isAR)
                {
                    var spawnLocation = commandBuffer.CreateEntity(entityInQueryIndex);
                    commandBuffer.AddComponent(entityInQueryIndex, spawnLocation, new SpawnPositionForARComponent {
                        spawnTranslation = translation.Value,
                        spawnRotation = rotation.Value
                    });
                }
            }
            // This means we have classified this Player prefab
            commandBuffer.AddComponent(entityInQueryIndex, entity, new PlayerClassifiedTag() );

        }).ScheduleParallel();

        m_BeginSimEcb.AddJobHandleForProducer(Dependency);
    }
}

Now we will update ARPoseSampler to check for a SpawnPositionForARComponent, and if it exists we update the pose to be behind the player spawn. We will use ARSessionOrigin's MakeContentAppearAt method, explained in Unity's AR Foundation documentation below:

MakeContentAppearAt(Transform, Quaternion)

Makes content appear to have orientation rotation relative to the Camera.

Declaration

public void MakeContentAppearAt(Transform content, Quaternion rotation)

Parameters

Remarks

This method does not actually change the Transform of content; instead, it updates the ARSessionOrigin's Transform so that the content appears to be in the requested orientation.

From AR Foundation's MakeContentAppearAt documentation

Because we don't want to actually move content, but instead want to move our AR Pose, we are going to provide the inverse of our translation and rotation to the MakeContentAppearAt method.

Also, we are going to need to save our updates to AR Session Origin. This is because we need to "undo" them before we add another update. That is because MakeContentAppearAt is additive, it does not reset on each call. So if we did not "undo" this, we would be pushing our pose further and further away.

  • Paste the code snippet below into ARPoseSampler.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.NetCode;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine.XR.ARFoundation;
using Unity.Mathematics;

public class ARPoseSampler : MonoBehaviour
{
    //We will be using ClientSimulationSystemGroup to update our ARPoseComponent
    private ClientSimulationSystemGroup m_ClientSimGroup;
    //We will be using Client World when destroying our SpawnPositionForARComponent
    private World m_ClientWorld;

    //This is the query we will use for SpawnPositionForARComponent
    private EntityQuery m_SpawnPositionQuery;
    //This is the AR Session Origin from the hierarchy that we will use to move the camera
    public ARSessionOrigin m_ARSessionOrigin;
    //We will save our updates to translation and rotation so we can "undo" them before our next update
    //We need to "undo" our updates because of how AR Session Origin MakeContentAppearAt() works
    private float3 m_LastTranslation = new float3(0,0,0); //We set the initial value to 0
    private quaternion m_LastRotation = new quaternion(0,0,0,1);  //We set the initial value to the identity
    
    void Start()
    {
        //We grab ClientSimulationSystemGroup to update ARPoseComponent in our Update loop
        foreach (var world in World.All)
        {
            if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
            {
                //Set our world
                m_ClientWorld = world;
                //We create the ARPoseComponent that we will update with new data
                world.EntityManager.CreateEntity(typeof(ARPoseComponent));
                //We grab the ClientSimulationSystemGroup for our Update loop
                m_ClientSimGroup = world.GetExistingSystem<ClientSimulationSystemGroup>();
                //Now we set our query for SpawnPositionForARComponent
                m_SpawnPositionQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<SpawnPositionForARComponent>());
            }
        }        
    }

    // Update is called once per frame
    void Update()
    {
        //We create a new Translation and Rotation from the transform of the GameObject
        //The GameObject Translation and Rotation is updated by the pose driver
        var arTranslation = new Translation {Value = transform.position};
        var arRotation = new Rotation {Value = transform.rotation};
        //Now we update our ARPoseComponent with the updated Pose Driver data
        var arPose = new ARPoseComponent {
           translation = arTranslation,
           rotation = arRotation 
        };
        m_ClientSimGroup.SetSingleton<ARPoseComponent>(arPose);

        //If the player was spawned, we will move the AR camera to behind the spawn location
        if(!m_SpawnPositionQuery.IsEmptyIgnoreFilter)
        {
            //We grab the component from the Singleton
            var spawnPosition = m_ClientSimGroup.GetSingleton<SpawnPositionForARComponent>();
            // Debug.Log("spawn position is: " + spawnPosition.spawnTranslation.ToString());     
            
            //We set the new pose to behind the player (0, 2, -10) (this is the same value the player is put in front of the pose in InputResponseMovementSystem)
            var newPoseTranslation = (spawnPosition.spawnTranslation) + (math.mul(spawnPosition.spawnRotation, new float3(0,2,0)).xyz) - (math.mul(spawnPosition.spawnRotation, new float3(0,0,10)).xyz);
            //The rotation will be the same
            var newPoseRotation = (spawnPosition.spawnRotation);
            
            // Debug.Log("calculated camera position is: " + newPoseTranslation.ToString());

            //MakeContentAppearAt requires a transform even though it is never used so we create a dummy transform
            Transform dummyTransform = new GameObject().transform;
            //First we will undo our last MakeContentAppearAt to go back to "normal"
            m_ARSessionOrigin.MakeContentAppearAt(dummyTransform, -1f*m_LastTranslation, Quaternion.Inverse(m_LastRotation));
            
            //Now we will update our LastTranslation and LastRotations to the values we are about to use
            //Because of how MakeContentAppearAt works we must do the inverse to move our camera where we want it
            m_LastTranslation = -1f * newPoseTranslation;
            m_LastRotation = Quaternion.Inverse(newPoseRotation);
            //Now that we have set the variables we will use them to adjust the AR pose
            m_ARSessionOrigin.MakeContentAppearAt(dummyTransform, m_LastTranslation, m_LastRotation);

            // Debug.Log("transform after MakeContentAppearAt: " + transform.position.ToString());
            //Now we delete the entity so this only runs during an initial spawn
            m_ClientWorld.EntityManager.DestroyEntity(m_ClientSimGroup.GetSingletonEntity<SpawnPositionForARComponent>());
        }
    }
}
  • Make sure MainScene is open, and then drag AR Session Origin from the Hierarchy into the appropriate field in the ARPoseSampler component within AR Camera (which is nested in AR Session Origin in Hierarchy)

  • Let's build and run for iOS and check it out

We are now able to re-spawn as an AR player

  • We created SpawnPositionForARComponent

  • We updated PlayerGhostSpawnClassificationSystem

  • We updated ARPoseSampler

Github branch link:

git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Updating-to-AR-Player'

Last updated