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

spawn an AR Player that is able to interact with desktop players by shooting and navigating
Your project will be able to make use of AR Player input configurations when deployed on ARKit enabled platforms.

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.
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:
1
using Unity.Entities;
2
using Unity.Collections;
3
using Unity.Transforms;
4
5
public struct ARPoseComponent : IComponentData
6
{
7
public Translation translation;
8
public Rotation rotation;
9
}
Copied!
Creating ARPoseComponent
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
Creating ARPoseSampler
    Paste the code snippet below into ARPoseSampler.cs:
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using Unity.Entities;
5
using Unity.NetCode;
6
using Unity.Jobs;
7
using Unity.Transforms;
8
using UnityEngine.XR.ARFoundation;
9
using Unity.Mathematics;
10
11
public class ARPoseSampler : MonoBehaviour
12
{
13
//We will be using ClientSimulationSystemGroup to update our ARPoseComponent
14
private ClientSimulationSystemGroup m_ClientSimGroup;
15
16
void Start()
17
{
18
//We grab ClientSimulationSystemGroup to update ARPoseComponent in our Update loop
19
foreach (var world in World.All)
20
{
21
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
22
{
23
//We create the ARPoseComponent that we will update with new data
24
world.EntityManager.CreateEntity(typeof(ARPoseComponent));
25
//We grab the ClientSimulationSystemGroup for our Update loop
26
m_ClientSimGroup = world.GetExistingSystem<ClientSimulationSystemGroup>();
27
}
28
}
29
}
30
31
// Update is called once per frame
32
void Update()
33
{
34
//We create a new Translation and Rotation from the transform of the GameObject
35
//The GameObject Translation and Rotation is updated by the pose driver
36
var arTranslation = new Translation {Value = transform.position};
37
var arRotation = new Rotation {Value = transform.rotation};
38
//Now we update our ARPoseComponent with the updated Pose Driver data
39
var arPose = new ARPoseComponent {
40
translation = arTranslation,
41
rotation = arRotation
42
};
43
m_ClientSimGroup.SetSingleton<ARPoseComponent>(arPose);
44
}
45
}
Copied!
Updating ARPoseSampler

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:
1
using Unity.Networking.Transport;
2
using Unity.NetCode;
3
using Unity.Burst;
4
using Unity.Entities;
5
using Unity. Transforms;
6
using Unity.Mathematics;
7
8
9
[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
10
public struct PlayerCommand : ICommandData
11
{
12
public uint Tick {get; set;}
13
public byte right;
14
public byte left;
15
public byte thrust;
16
public byte reverseThrust;
17
public byte selfDestruct;
18
public byte shoot;
19
public float mouseX;
20
public float mouseY;
21
public byte isAR;
22
public float3 arTranslation;
23
public quaternion arRotation;
24
}
25
Copied!
Updating PlayerCommand
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:
1
using UnityEngine;
2
using Unity.Entities;
3
using Unity.Transforms;
4
using Unity.NetCode;
5
using Unity.Jobs;
6
using Unity.Collections;
7
using UnityEngine.XR;
8
using Unity.Physics;
9
10
[UpdateInGroup(typeof(GhostInputSystemGroup))]
11
public class ARInputSystem : SystemBase
12
{
13
//We will need a command buffer for structural changes
14
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
15
//We will grab the ClientSimulationSystemGroup because we require its tick in the ICommandData
16
private ClientSimulationSystemGroup m_ClientSimulationSystemGroup;
17
18
protected override void OnCreate()
19
{
20
//We set our variables
21
m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
22
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
23
//We will only run this system if the player is in game and if the palyer is an AR player
24
RequireSingletonForUpdate<NetworkStreamInGame>();
25
RequireSingletonForUpdate<IsARPlayerComponent>();
26
}
27
28
protected override void OnUpdate()
29
{
30
//The only inputs is for shooting or for self destruction
31
//Movement will be through the ARPoseComponent
32
byte selfDestruct, shoot;
33
selfDestruct = shoot = 0;
34
35
//More than 2 touches will register as self-destruct
36
if (Input.touchCount > 2)
37
{
38
selfDestruct = 1;
39
}
40
//A single touch will register as shoot
41
if (Input.touchCount == 1)
42
{
43
shoot = 1;
44
}
45
46
//We grab the AR pose to send to the server for movement
47
var arPoseDriver = GetSingleton<ARPoseComponent>();
48
//We must declare our local variables before the .ForEach()
49
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
50
var inputFromEntity = GetBufferFromEntity<PlayerCommand>();
51
var inputTargetTick = m_ClientSimulationSystemGroup.ServerTick;
52
53
Entities
54
.WithAll<OutgoingRpcDataStreamBufferComponent>()
55
.WithNone<NetworkStreamDisconnected>()
56
.ForEach((Entity entity, int nativeThreadIndex, in CommandTargetComponent state) =>
57
{
58
//This is the same pattern as a desktop player
59
//If targetEntity is null we spawn a new player
60
if (state.targetEntity == Entity.Null)
61
{
62
if (shoot != 0)
63
{
64
var req = commandBuffer.CreateEntity(nativeThreadIndex);
65
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(nativeThreadIndex, req);
66
commandBuffer.AddComponent(nativeThreadIndex, req, new SendRpcCommandRequestComponent {TargetConnection = entity});
67
}
68
}
69
else
70
{
71
//We provide our PlayerCommand to be sent to the server
72
if (inputFromEntity.HasComponent(state.targetEntity))
73
{
74
var input = inputFromEntity[state.targetEntity];
75
input.AddCommandData(new PlayerCommand{Tick = inputTargetTick,
76
selfDestruct = selfDestruct, shoot = shoot,
77
isAR = 1,
78
arTranslation = arPoseDriver.translation.Value,
79
arRotation = arPoseDriver.rotation.Value});
80
}
81
}
82
}).Schedule();
83
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
84
}
85
}
Copied!

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:
1
using Unity.Entities;
2
using Unity.Mathematics;
3
using Unity.Transforms;
4
using Unity.NetCode;
5
using Unity.Networking.Transport.Utilities;
6
using Unity.Collections;
7
using Unity.Physics;
8
using Unity.Jobs;
9
using UnityEngine;
10
11
//InputResponseMovementSystem runs on both the Client and Server
12
//It is predicted on the client but "decided" on the server
13
[UpdateInWorld(UpdateInWorld.TargetWorld.ClientAndServer)]
14
public class InputResponseMovementSystem : SystemBase
15
{
16
//This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
17
private GhostPredictionSystemGroup m_PredictionGroup;
18
19
20
protected override void OnCreate()
21
{
22
// m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
23
24
//We will grab this system so we can use its "prediction tick" and "DeltaTime"
25
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
26
27
}
28
29
protected override void OnUpdate()
30
{
31
//No need for a CommandBuffer because we are not making any structural changes to any entities
32
//We are setting values on components that already exist
33
// var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
34
35
//These are special NetCode values needed to work the prediction system
36
var currentTick = m_PredictionGroup.PredictingTick;
37
var deltaTime = m_PredictionGroup.Time.DeltaTime;
38
39
//We must declare our local variables before the .ForEach()
40
var playerForce = GetSingleton<GameSettingsComponent>().playerForce;
41
42
//We will grab the buffer of player commands from the player entity
43
var inputFromEntity = GetBufferFromEntity<PlayerCommand>(true);
44
45
//We are looking for player entities that have PlayerCommands in their buffer
46
Entities
47
.WithReadOnly(inputFromEntity)
48
.WithAll<PlayerTag, PlayerCommand>()
49
.ForEach((Entity entity, int nativeThreadIndex, ref Translation translation, ref Rotation rotation, ref VelocityComponent velocity,
50
in GhostOwnerComponent ghostOwner, in PredictedGhostComponent prediction) =>
51
{
52
//Here we check if we SHOULD do the prediction based on the tick, if we shouldn't, we return
53
if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
54
return;
55
56
//We grab the buffer of commands from the player entity
57
var input = inputFromEntity[entity];
58
59
//We then grab the Command from the current tick (which is the PredictingTick)
60
//if we cannot get it at the current tick we make sure shoot is 0
61
//This is where we will store the current tick data
62
PlayerCommand inputData;
63
if (!input.GetDataAtTick(currentTick, out inputData))
64
inputData.shoot = 0;
65
66
if (inputData.right == 1)
67
{ //thrust to the right of where the player is facing
68
velocity.Linear += math.mul(rotation.Value, new float3(1,0,0)).xyz * playerForce * deltaTime;
69
}
70
if (inputData.left == 1)
71
{ //thrust to the left of where the player is facing
72
velocity.Linear += math.mul(rotation.Value, new float3(-1,0,0)).xyz * playerForce * deltaTime;
73
}
74
if (inputData.thrust == 1)
75
{ //thrust forward of where the player is facing
76
velocity.Linear += math.mul(rotation.Value, new float3(0,0,1)).xyz * playerForce * deltaTime;
77
}
78
if (inputData.reverseThrust == 1)
79
{ //thrust backwards of where the player is facing
80
velocity.Linear += math.mul(rotation.Value, new float3(0,0,-1)).xyz * playerForce * deltaTime;
81
}
82
83
84
if (inputData.mouseX != 0 || inputData.mouseY != 0)
85
{ //move the mouse
86
//here we have "hardwired" the look speed, we could have included this in the GameSettingsComponent to make it configurable
87
float lookSpeedH = 2f;
88
float lookSpeedV = 2f;
89
Quaternion currentQuaternion = rotation.Value;
90
float yaw = currentQuaternion.eulerAngles.y;
91
float pitch = currentQuaternion.eulerAngles.x;
92
93
//MOVING WITH MOUSE
94
yaw += lookSpeedH * inputData.mouseX;
95
pitch -= lookSpeedV * inputData.mouseY;
96
Quaternion newQuaternion = Quaternion.identity;
97
newQuaternion.eulerAngles = new Vector3(pitch,yaw, 0);
98
rotation.Value = newQuaternion;
99
}
100
//If the PlayerCommand is from an AR player we will update movement in a special way
101
if (inputData.isAR == 1)
102
{
103
//The player is going to be (0,-2,10) relative to the AR pose
104
//This will make the player appear a bit lower and in front of the camera, making it easier to control
105
translation.Value = (inputData.arTranslation) - (math.mul(rotation.Value, new float3(0,2,0)).xyz) + (math.mul(rotation.Value, new float3(0,0,10)).xyz);
106
//The player will face the same direction as the camera
107
rotation.Value = (inputData.arRotation);
108
}
109
110
}).ScheduleParallel();
111
112
//No need to .AddJobHandleForProducer() because we did not need a CommandBuffer to make structural changes
113
}
114
}
Copied!
Updating InputResponseMovementSystem
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:
1
using Unity.Collections;
2
using Unity.Entities;
3
using Unity.Jobs;
4
using Unity.NetCode;
5
using UnityEngine;
6
using Unity.Transforms;
7
using Unity.Mathematics;
8
9
//We are updating only in the client world because only the client must specify exactly which player entity it "owns"
10
[UpdateInWorld(UpdateInWorld.TargetWorld.Client)]
11
//We will be updating after NetCode's GhostSpawnClassificationSystem because we want
12
//to ensure that the PredictedGhostComponent (which it adds) is available on the player entity to identify it
13
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
14
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
15
public class PlayerGhostSpawnClassificationSystem : SystemBase
16
{
17
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
18
19
//We will store the Camera prefab here which we will attach when we identify our player entity
20
private Entity m_CameraPrefab;
21
22
protected override void OnCreate()
23
{
24
m_BeginSimEcb = World.GetExistingSystem<BeginSimulationEntityCommandBufferSystem>();
25
26
//We need to make sure we have NCE before we start the update loop (otherwise it's unnecessary)
27
RequireSingletonForUpdate<NetworkIdComponent>();
28
RequireSingletonForUpdate<CameraAuthoringComponent>();
29
}
30
31
struct GhostPlayerState : ISystemStateComponentData
32
{
33
}
34
35
36
protected override void OnUpdate()
37
{
38
//Here we set the prefab we will use
39
if (m_CameraPrefab == Entity.Null)
40
{
41
//We grab our camera and set our variable
42
m_CameraPrefab = GetSingleton<CameraAuthoringComponent>().Prefab;
43
return;
44
}
45
46
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
47
48
//We must declare our local variables before using them
49
var camera = m_CameraPrefab;
50
//The "playerEntity" is the NCE
51
var playerEntity = GetSingletonEntity<NetworkIdComponent>();
52
//The false is to signify that the data will NOT be read-only
53
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
54
//Check if this is an AR player
55
IsARPlayerComponent arComponent;
56
var isAR = TryGetSingleton<IsARPlayerComponent>(out arComponent);
57
58
//We search for player entities with a PredictedGhostComponent (which means it is ours)
59
Entities
60
.WithAll<PlayerTag, PredictedGhostComponent>()
61
.WithNone<GhostPlayerState>()
62
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
63
.ForEach((Entity entity, int entityInQueryIndex, in Translation translation, in Rotation rotation) =>
64
{
65
//Here is where we update the NCE's CommandTargetComponent targetEntity to point at our player entity
66
var state = commandTargetFromEntity[playerEntity];
67
state.targetEntity = entity;
68
commandTargetFromEntity[playerEntity] = state;
69
70
//Here we add a special "ISystemStateComponentData" component
71
//This component does NOT get deleted with the rest of the entity
72
//Unity provided this special type of component to provide us a way to do "clean up" when important entities are destroyed
73
//In our case we will use GhostPlayerState to know when our player entity has been destroyed and will
74
//allow us to set our NCE CommandTargetComponent's targetEntity field back to null
75
//It also helps us ensure that we only set up our player entity once because we have a
76
//.WithNone<GhostPlayerState>() on our entity query
77
commandBuffer.AddComponent(entityInQueryIndex, entity, new GhostPlayerState());
78
79
if(!isAR)
80
{
81
//This creates our camera
82
var cameraEntity = commandBuffer.Instantiate(entityInQueryIndex, camera);
83
//This is how you "attach" a prefab entity to another
84
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new Parent { Value = entity });
85
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new LocalToParent() );
86
}
87
88
}).ScheduleParallel();
89
90
//This .ForEach() looks for Entities with a GhostPlayerState but no Snapshot data
91
//Because GhostPlayerState is an ISystemStateComponentData component, it does not get deleted with the rest of the entity
92
//It must be manually deleted
93
//By using GhostPlayerState we are able to "clean up" on the client side and clear our targetEntity in our NCE's CommandTargetComponent
94
//This allows us to "reset" and create a PlayerSpawnRequestRpc again when we hit spacebar (targetEntity must equal null to trigger the RPC)
95
Entities.WithNone<SnapshotData>()
96
.WithAll<GhostPlayerState>()
97
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
98
.ForEach((Entity ent, int entityInQueryIndex) =>
99
{
100
var commandTarget = commandTargetFromEntity[playerEntity];
101
102
if (ent == commandTarget.targetEntity)
103
{
104
commandTarget.targetEntity = Entity.Null;
105
commandTargetFromEntity[playerEntity] = commandTarget;
106
}
107
commandBuffer.RemoveComponent<GhostPlayerState>(entityInQueryIndex, ent);
108
}).ScheduleParallel();
109
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
110
111
112
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
113
}
114
}
Copied!
Updating PlayerGhostSpawnClassificationSystem
    Let's hit play, host a game, and spawn a player
Hitting play and our camera not appearing
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;"
    Click away from the Scripting Define Symbols field in order for this update to take
    Save!
Updating "Player Settings..." for our iOS build with HYBRID_ENTITIES_CAMERA_CONVERSION
    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
After switching back to development platform player camera works again
    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)
Selecting our iOS-Build and hitting Build and Run to switch back
    Once the Editor resets to "iOS" hit play, host a game, and spawn a player
Spawning a player now generates the camera again
    Now click "Build and Run" again to build to Xcode
Hitting Build and Run after the Editor reset to iOS
    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
Updated AR Player is able to spawn, navigate, and fire
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:
1
using Unity.Entities;
2
using UnityEngine;
3
using Unity.Mathematics;
4
5
public struct SpawnPositionForARComponent : IComponentData
6
{
7
public float3 spawnTranslation;
8
public quaternion spawnRotation;
9
}
Copied!
    Now we need to update PlayerGhostSpawnClassificationSystem in order to create a singleton with the spawn position
    Paste the code snippet below into PlayerGhostSpawnClassificationSystem.cs:
1
using Unity.Collections;
2
using Unity.Entities;
3
using Unity.Jobs;
4
using Unity.NetCode;
5
using UnityEngine;
6
using Unity.Transforms;
7
using Unity.Mathematics;
8
9
//We are updating only in the client world because only the client must specify exactly which player entity it "owns"
10
[UpdateInWorld(UpdateInWorld.TargetWorld.Client)]
11
//We will be updating after NetCode's GhostSpawnClassificationSystem because we want
12
//to ensure that the PredictedGhostComponent (which it adds) is available on the player entity to identify it
13
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
14
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
15
public class PlayerGhostSpawnClassificationSystem : SystemBase
16
{
17
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
18
19
//We will store the Camera prefab here which we will attach when we identify our player entity
20
private Entity m_CameraPrefab;
21
22
protected override void OnCreate()
23
{
24
m_BeginSimEcb = World.GetExistingSystem<BeginSimulationEntityCommandBufferSystem>();
25
26
//We need to make sure we have NCE before we start the update loop (otherwise it's unnecessary)
27
RequireSingletonForUpdate<NetworkIdComponent>();
28
RequireSingletonForUpdate<CameraAuthoringComponent>();
29
}
30
31
struct GhostPlayerState : ISystemStateComponentData
32
{
33
}
34
35
36
protected override void OnUpdate()
37
{
38
//Here we set the prefab we will use
39
if (m_CameraPrefab == Entity.Null)
40
{
41
//We grab our camera and set our variable
42
m_CameraPrefab = GetSingleton<CameraAuthoringComponent>().Prefab;
43
return;
44
}
45
46
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
47
48
//We must declare our local variables before using them
49
var camera = m_CameraPrefab;
50
//The "playerEntity" is the NCE
51
var playerEntity = GetSingletonEntity<NetworkIdComponent>();
52
//The false is to signify that the data will NOT be read-only
53
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
54
//Check if this is an AR player
55
IsARPlayerComponent arComponent;
56
var isAR = TryGetSingleton<IsARPlayerComponent>(out arComponent);
57
58
//We search for player entities with a PredictedGhostComponent (which means it is ours)
59
Entities
60
.WithAll<PlayerTag, PredictedGhostComponent>()
61
.WithNone<GhostPlayerState>()
62
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
63
.ForEach((Entity entity, int entityInQueryIndex, in Translation translation, in Rotation rotation) =>
64
{
65
//Here is where we update the NCE's CommandTargetComponent targetEntity to point at our player entity
66
var state = commandTargetFromEntity[playerEntity];
67
state.targetEntity = entity;
68
commandTargetFromEntity[playerEntity] = state;
69
70
//Here we add a special "ISystemStateComponentData" component
71
//This component does NOT get deleted with the rest of the entity
72
//Unity provided this special type of component to provide us a way to do "clean up" when important entities are destroyed
73
//In our case we will use GhostPlayerState to know when our player entity has been destroyed and will
74
//allow us to set our NCE CommandTargetComponent's targetEntity field back to null
75
//It also helps us ensure that we only set up our player entity once because we have a
76
//.WithNone<GhostPlayerState>() on our entity query
77
commandBuffer.AddComponent(entityInQueryIndex, entity, new GhostPlayerState());
78
79
if(!isAR)
80
{
81
//This creates our camera
82
var cameraEntity = commandBuffer.Instantiate(entityInQueryIndex, camera);
83
//This is how you "attach" a prefab entity to another
84
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new Parent { Value = entity });
85
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new LocalToParent() );
86
}
87
//If we are an AR player we will create SpawnPositionForARoComponent
88
if (isAR)
89
{
90
var spawnLocation = commandBuffer.CreateEntity(entityInQueryIndex);
91
commandBuffer.AddComponent(entityInQueryIndex, spawnLocation, new SpawnPositionForARComponent {
92
spawnTranslation = translation.Value,
93
spawnRotation = rotation.Value
94
});
95
}
96
97
}).ScheduleParallel();
98
99
//This .ForEach() looks for Entities with a GhostPlayerState but no Snapshot data
100
//Because GhostPlayerState is an ISystemStateComponentData component, it does not get deleted with the rest of the entity
101
//It must be manually deleted
102
//By using GhostPlayerState we are able to "clean up" on the client side and clear our targetEntity in our NCE's CommandTargetComponent
103
//This allows us to "reset" and create a PlayerSpawnRequestRpc again when we hit spacebar (targetEntity must equal null to trigger the RPC)
104
Entities.WithNone<SnapshotData>()
105
.WithAll<GhostPlayerState>()
106
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
107
.ForEach((Entity ent, int entityInQueryIndex) =>
108
{
109
var commandTarget = commandTargetFromEntity[playerEntity];
110
111
if (ent == commandTarget.targetEntity)
112
{
113
commandTarget.targetEntity = Entity.Null;
114
commandTargetFromEntity[playerEntity] = commandTarget;
115
}
116
commandBuffer.RemoveComponent<GhostPlayerState>(entityInQueryIndex, ent);
117
}).ScheduleParallel();
118
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
119
120
121
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
122
}
123
}
Copied!
Updating PlayerGhostSpawnClassificationSystem to provide spawn location
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, Vector3, Quaternion)
Makes content appear to be placed at position with orientation rotation.
Declaration
1
public void MakeContentAppearAt(Transform content, Vector3 position, Quaternion rotation)
Copied!
Parameters
Type
Name
Description
Transform
content
The Transform of the content you wish to affect.
Vector3
position
The position you wish the content to appear at. This could be a position on a detected plane, for example.
rotation
The rotation the content should appear to be in, relative to the Camera.
Remarks
This method does not actually change the Transform of content; instead, it updates the ARSessionOrigin's Transform such that it appears the content is now at the given position and rotation. This is useful for placing AR content onto surfaces when the content itself cannot be moved at runtime. For example, if your content includes terrain or a nav mesh, then it cannot be moved or rotated dynamically.
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:
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using Unity.Entities;
5
using Unity.NetCode;
6
using Unity.Jobs;
7
using Unity.Transforms;
8
using UnityEngine.XR.ARFoundation;
9
using Unity.Mathematics;
10
11
public class ARPoseSampler : MonoBehaviour
12
{
13
//We will be using ClientSimulationSystemGroup to update our ARPoseComponent
14
private ClientSimulationSystemGroup m_ClientSimGroup;
15
//We will be using Client World when destroying our SpawnPositionForARComponent
16
private World m_ClientWorld;
17
18
//This is the query we will use for SpawnPositionForARComponent
19
private EntityQuery m_SpawnPositionQuery;
20
//This is the AR Session Origin from the hierarchy that we will use to move the camera
21
public ARSessionOrigin m_ARSessionOrigin;
22
//We will save our updates to translation and rotation so we can "undo" them before our next update
23
//We need to "undo" our updates because of how AR Session Origin MakeContentAppearAt() works
24
private float3 m_LastTranslation = new float3(0,0,0); //We set the initial value to 0
25
private quaternion m_LastRotation = new quaternion(0,0,0,1); //We set the initial value to the identity
26
27
void Start()
28
{
29
//We grab ClientSimulationSystemGroup to update ARPoseComponent in our Update loop
30
foreach (var world in World.All)
31
{
32
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
33
{
34
//Set our world
35
m_ClientWorld = world;
36
//We create the ARPoseComponent that we will update with new data
37
world.EntityManager.CreateEntity(typeof(ARPoseComponent));
38
//We grab the ClientSimulationSystemGroup for our Update loop
39
m_ClientSimGroup = world.GetExistingSystem<ClientSimulationSystemGroup>();
40
//Now we set our query for SpawnPositionForARComponent
41
m_SpawnPositionQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<SpawnPositionForARComponent>());
42
}
43
}
44
}
45
46
// Update is called once per frame
47
void Update()
48
{
49
//We create a new Translation and Rotation from the transform of the GameObject
50
//The GameObject Translation and Rotation is updated by the pose driver
51
var arTranslation = new Translation {Value = transform.position};
52
var arRotation = new Rotation {Value = transform.rotation};
53
//Now we update our ARPoseComponent with the updated Pose Driver data
54
var arPose = new ARPoseComponent {
55
translation = arTranslation,
56
rotation = arRotation
57
};
58
m_ClientSimGroup.SetSingleton<ARPoseComponent>(arPose);
59
60
//If the player was spawned, we will move the AR camera to behind the spawn location
61
if(!m_SpawnPositionQuery.IsEmptyIgnoreFilter)
62
{
63
//We grab the component from the Singleton
64
var spawnPosition = m_ClientSimGroup.GetSingleton<SpawnPositionForARComponent>();
65
// Debug.Log("spawn position is: " + spawnPosition.spawnTranslation.ToString());
66
67
//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)
68
var newPoseTranslation = (spawnPosition.spawnTranslation) + (math.mul(spawnPosition.spawnRotation, new float3(0,2,0)).xyz) - (math.mul(spawnPosition.spawnRotation, new float3(0,0,10)).xyz);
69
//The rotation will be the same
70
var newPoseRotation = (spawnPosition.spawnRotation);
71
72
// Debug.Log("calculated camera position is: " + newPoseTranslation.ToString());
73
74
//MakeContentAppearAt requires a transform even though it is never used so we create a dummy transform
75
Transform dummyTransform = new GameObject().transform;
76
//First we will undo our last MakeContentAppearAt to go back to "normal"
77
m_ARSessionOrigin.MakeContentAppearAt(dummyTransform, -1f*m_LastTranslation, Quaternion.Inverse(m_LastRotation));
78
79
//Now we will update our LastTranslation and LastRotations to the values we are about to use
80
//Because of how MakeContentAppearAt works we must do the inverse to move our camera where we want it
81
m_LastTranslation = -1f * newPoseTranslation;
82
m_LastRotation = Quaternion.Inverse(newPoseRotation);
83
//Now that we have set the variables we will use them to adjust the AR pose
84
m_ARSessionOrigin.MakeContentAppearAt(dummyTransform, m_LastTranslation, m_LastRotation);
85
86
// Debug.Log("transform after MakeContentAppearAt: " + transform.position.ToString());
87
//Now we delete the entity so this only runs during an initial spawn
88
m_ClientWorld.EntityManager.DestroyEntity(m_ClientSimGroup.GetSingletonEntity<SpawnPositionForARComponent>());
89
}
90
}
91
}
Copied!
Updating ARPoseSampler to ingest 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)
Updating ARPoseSamplers public field with AR Session Orign
    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 modified 7mo ago