Keep Score with NetCode
Code and workflows to keep score between players in a multiplayer game on a LAN

What you'll develop on this page

Server keeping score and sending the score to clients to update their UI
The server will set and adjust player scores based on bullet collisions. It will also track the highest score.

How we'll be keeping score

Scoring

1 point for shooting asteroids (as many as you can get through before disappear)
10 points for shooting a player

How the server will keep score

Server will be keeping score by updating ghosted HighScore ghosts and a single HighestScore ghost. When players join, a new object is created.
HighestScore is pre-spawned in ConvertedScene empty (does it have to be prefab?)
PlayerScores are created when a new player joins.
We will be using hot-off-the-press functionality from NetCode v0.6: the ability to treat DynamicBuffers as ghosts. We we can have a single buffer of scores and this buffer will be replicated across the network.

Recycling HighScore ghosts

NCEs re-use network ids, so we will need to keep track of this.
If a player joins the session and it is re-using a network id, we clear that player score.

Server creating scores

We are going to start by setting up our HighestScore in ConvertedSubScene.
    First, let's create the component we will be referencing and updating to keep track of the server's highest score
    Create HighestScoreComponent in the Mixed/Components folder
    Paste the code snippet below into HighestScoreComponent.cs:
1
using Unity.Networking.Transport;
2
using Unity.NetCode;
3
using Unity.Burst;
4
using Unity.Entities;
5
using Unity.Collections;
6
7
[GenerateAuthoringComponent]
8
public struct HighestScoreComponent : IComponentData
9
{
10
[GhostField]
11
public FixedString64 playerName;
12
13
[GhostField]
14
public int highestScore;
15
}
Copied!
    Navigate to ConvertedSubScene and create an empty GameObject called HighestScore
    Add a GhostAuthoringComponent (by clicking Add Component button in Inspector when HighestScore is selected in Hierarchy)
      Name = HighestScore
      Importance = 500
      Supported Ghost Mode = Interpolated
      Optimization Mode = Static
    Next add HighestScoreComponent
    When done with those steps, drag HighestScore GameObject from Hierarchy into Scripts and Prefabs
Creating HighScore GameObject and prefab
    Even though we will not make another HighestScore, NetCode requires that the ghosted GameObject be a prefab
    Now let's create the PlayerScore ghosts that we will be using to keep track of individual player scores
    Create PlayerScoreAuthoringComponent in Server/Components
    Paste the code snippet below into PlayerScoreAuthoringComponent.cs:
1
using Unity.Entities;
2
3
[GenerateAuthoringComponent]
4
public struct PlayerScoreAuthoringComponent : IComponentData
5
{
6
public Entity Prefab;
7
}
Copied!
Creating PlayerScoreAuthoringComponent
    Next let's create the component that will be storing the actual player data named PlayerScoreComponent in the Mixed/Components folder
    Paste the code snippet below into PlayerScoreComponent.cs:
1
using Unity.Networking.Transport;
2
using Unity.NetCode;
3
using Unity.Burst;
4
using Unity.Entities;
5
using Unity.Collections;
6
7
[GenerateAuthoringComponent]
8
public struct PlayerScoreComponent : IComponentData
9
{
10
[GhostField]
11
public int networkId;
12
[GhostField]
13
public FixedString64 playerName;
14
[GhostField]
15
public int currentScore;
16
[GhostField]
17
public int highScore;
18
}
Copied!
Creating PlayerScoreComponent
    Create an empty GameObject called PlayerScore in the Hierarchy (doesn't matter which one)
    Add PlayerScoreComponent to PlayerScore
    Add GhostAuthoringComponent
      Name = PlayerScore
      Importance = 500
      Supported Ghost Modes = Interpolated
      Optimization Mode = Static
    Drag it into Scripts and Prefabs
    Delete it from the hierarchy
Creating PlayerScore prefab
    Navigate to ConvertedSubScene and add PlayerScoreAuthoringComponent to the PrefabCollection GameObject
    Drag the PlayerScore prefab from the Scripts and Prefabs folder into the "Prefab" field in the Player Score Authoring Component in Inspector when PrefabCollection is selected in Hierarchy
Updating PrefabCollection with PlayerScore
    Now we have our ghosts ready
      HighestScore is part of the ConvertedSubScene
      PlayerScore is referenced as part of PrefabCollection
We are going to kick off the process of setting up scores by adding a new RPC to be sent in ClientLoadGameSystem.
We could add this flow to existing flows that are kicked off by ClientLoadGameSystem (like SendServerGameLoadedRpc), but we are going to keep it separate to keep this flow concerned only with score setup.
    Let's create SendServerPlayerNameRpc in the Mixed/Commands folder
    Paste the code snippet below into SendServerPlayerNameRpc.cs:
1
using AOT;
2
using Unity.Burst;
3
using Unity.Networking.Transport;
4
using Unity.NetCode;
5
using Unity.Entities;
6
using Unity.Collections;
7
using System.Collections;
8
using System;
9
10
public struct SendServerPlayerNameRpc : IRpcCommand
11
{
12
public FixedString64 playerName;
13
}
Copied!
Creating SendServerPlayerNameRpc
    Now let's update ClientLoadGameSystem to send this RPC as part of the game loading process
    Paste the code snippet below into ClientLoadGameSystem.cs:
1
using Unity.Entities;
2
using Unity.NetCode;
3
using UnityEngine;
4
5
//This will only run on the client because it updates in ClientSimulationSystemGroup (which the server does not have)
6
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
7
[UpdateBefore(typeof(RpcSystem))]
8
public class ClientLoadGameSystem : SystemBase
9
{
10
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
11
12
protected override void OnCreate()
13
{
14
//We will be using the BeginSimECB
15
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
16
17
//Requiring the ReceiveRpcCommandRequestComponent ensures that update is only run when an NCE exists and a SendClientGameRpc has come in
18
RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly<SendClientGameRpc>(), ComponentType.ReadOnly<ReceiveRpcCommandRequestComponent>()));
19
//This is just here to make sure the Sub Scene is streamed in before the client sets up the level data
20
RequireSingletonForUpdate<GameSettingsComponent>();
21
//We will make sure we have our ClientDataComponent so we can send the server our player name
22
RequireSingletonForUpdate<ClientDataComponent>();
23
}
24
25
protected override void OnUpdate()
26
{
27
28
//We must declare our local variables before using them within a job (.ForEach)
29
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
30
var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
31
var gameSettingsEntity = GetSingletonEntity<GameSettingsComponent>();
32
var getGameSettingsComponentData = GetComponentDataFromEntity<GameSettingsComponent>();
33
var clientData = GetSingleton<ClientDataComponent>(); //We will use this to send the player name to server
34
35
Entities
36
.ForEach((Entity entity, in SendClientGameRpc request, in ReceiveRpcCommandRequestComponent requestSource) =>
37
{
38
//This destroys the incoming RPC so the code is only run once
39
commandBuffer.DestroyEntity(entity);
40
41
//Check for disconnects before moving forward
42
if (!rpcFromEntity.HasComponent(requestSource.SourceConnection))
43
return;
44
45
//Set the game size (unnecessary right now but we are including it to show how it is done)
46
getGameSettingsComponentData[gameSettingsEntity] = new GameSettingsComponent
47
{
48
levelWidth = request.levelWidth,
49
levelHeight = request.levelHeight,
50
levelDepth = request.levelDepth,
51
playerForce = request.playerForce,
52
bulletVelocity = request.bulletVelocity
53
};
54
55
56
//Here we create a new singleton entity for GameNameComponent
57
//We could add this component to the singleton entity that has the GameSettingsComponent
58
//but we will keep them separate in case we want to change workflows in the future and don't
59
//want these components to be dependent on the same entity
60
var gameNameEntity= commandBuffer.CreateEntity();
61
commandBuffer.AddComponent(gameNameEntity, new GameNameComponent {
62
GameName = request.gameName
63
});
64
65
//These update the NCE with NetworkStreamInGame (required to start receiving snapshots) and
66
//PlayerSpawningStateComponent, which we will use when we spawn players
67
commandBuffer.AddComponent(requestSource.SourceConnection, new PlayerSpawningStateComponent());
68
commandBuffer.AddComponent(requestSource.SourceConnection, default(NetworkStreamInGame));
69
70
//This tells the server "I loaded the level"
71
//First we create an entity called levelReq that will have 2 necessary components
72
//Next we add the RPC we want to send (SendServerGameLoadedRpc) and then we add
73
//SendRpcCommandRequestComponent with our TargetConnection being the NCE with the server (which will send it to the server)
74
var levelReq = commandBuffer.CreateEntity();
75
commandBuffer.AddComponent(levelReq, new SendServerGameLoadedRpc());
76
commandBuffer.AddComponent(levelReq, new SendRpcCommandRequestComponent {TargetConnection = requestSource.SourceConnection});
77
78
// this tells the server "This is my name and Id" which will be used for player score tracking
79
var playerReq = commandBuffer.CreateEntity();
80
commandBuffer.AddComponent(playerReq, new SendServerPlayerNameRpc {playerName = clientData.PlayerName});
81
commandBuffer.AddComponent(playerReq, new SendRpcCommandRequestComponent {TargetConnection = requestSource.SourceConnection});
82
83
}).Schedule();
84
85
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
86
}
87
}
Copied!
Updating ClientLoadLevelSystem
    Although there will be only one HighestScore entity, there will be a PlayerScore created for each unique NCE network id value
      Remember that NetCode recycles network id's once players have disconnected
      So a player may have its score tracked during gameplay, then leave the game, and then a new player may join with that same network id (i.e. it's been 'recycled')
      So we have to set up a process that checks our existing PlayerScores network id values to see if they match the new SendServerPlayerNameRpc's NCE Network id
        If they do match, we will reset that PlayerScore to 0 and update it with the sent RPCs provided name
    Create SetupScoreSystem in the Server/Systems folder
    Paste the code snippet below into SetupScoreSystem.cs:
1
using Unity.Collections;
2
using Unity.Entities;
3
using Unity.NetCode;
4
using UnityEngine;
5
using Unity.Burst;
6
using Unity.Jobs;
7
8
//The server will be keeping score
9
//The client will only read the scores and update overlay
10
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
11
public class SetupScoreSystem : SystemBase
12
{
13
//We will be making structural changes so we need a command buffer
14
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
15
16
//This will be the query for the highest score
17
private EntityQuery m_HighestScoreQuery;
18
19
//This will be the query for the palyer scores
20
private EntityQuery m_PlayerScoresQuery;
21
22
//This will be the prefab used to create PlayerScores
23
private Entity m_Prefab;
24
25
protected override void OnCreate()
26
{
27
//We set the command buffer
28
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
29
30
//This will be used to check if there is already HighestScore (initialization)
31
m_HighestScoreQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly<HighestScoreComponent>());
32
33
//This will be used to check if there are already PlayerScores (initialization)
34
m_PlayerScoresQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly<PlayerScoreComponent>());
35
36
//We are going to wait to initialize and update until the first player connects and sends their name
37
RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly<SendServerPlayerNameRpc>(), ComponentType.ReadOnly<ReceiveRpcCommandRequestComponent>()));
38
}
39
40
protected override void OnUpdate()
41
{
42
//Here we set the prefab we will use
43
if (m_Prefab == Entity.Null)
44
{
45
//We grab the converted PrefabCollection Entity's PlayerScoreAuthoringComponent
46
//and set m_Prefab to its Prefab value
47
m_Prefab = GetSingleton<PlayerScoreAuthoringComponent>().Prefab;
48
//We then initialize by creating the first PlayerScore
49
var initialPlayerScore = EntityManager.Instantiate(m_Prefab);
50
//We set the initial player score to 1 so the first player will be assigned this PlayerScore
51
EntityManager.SetComponentData<PlayerScoreComponent>(initialPlayerScore, new PlayerScoreComponent{
52
networkId = 1
53
});
54
//we must "return" after setting this prefab because if we were to continue into the Job
55
//we would run into errors because the variable was JUST set (ECS funny business)
56
//comment out return and see the error
57
return;
58
}
59
60
//We need to declare our local variables before the .ForEach()
61
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
62
//We use this to check for disconnects
63
var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
64
//We are going to grab all existing player scores because we need to check if the new player has an old NetworkId
65
var currentPlayerScoreEntities = m_PlayerScoresQuery.ToEntityArray(Allocator.TempJob);
66
//We are going to need to grab the Player score from the entity
67
var playerScoreComponent = GetComponentDataFromEntity<PlayerScoreComponent>();
68
//We grab the prefab in case we need to create a new PlayerScore for a new NetworkId
69
var scorePrefab = m_Prefab;
70
//We are going to need to be able to grab the NetworkIdComponent from the RPC source to know what the player's NetworkId is
71
var networkIdFromEntity = GetComponentDataFromEntity<NetworkIdComponent>();
72
73
Entities
74
.WithDisposeOnCompletion(currentPlayerScoreEntities)
75
.ForEach((Entity entity, in SendServerPlayerNameRpc request, in ReceiveRpcCommandRequestComponent requestSource) =>
76
{
77
//Delete the rpc
78
commandBuffer.DestroyEntity(entity);
79
80
//Check for disconnects
81
if (!rpcFromEntity.HasComponent(requestSource.SourceConnection))
82
return;
83
84
//Grab the NetworkIdComponent's Value
85
var newPlayersNetworkId = networkIdFromEntity[requestSource.SourceConnection].Value;
86
87
//We create a clean PlayerScore component with the player's name and the player's NetworkId value
88
var newPlayerScore = new PlayerScoreComponent{
89
networkId = newPlayersNetworkId,
90
playerName = request.playerName,
91
currentScore = 0,
92
highScore = 0
93
};
94
95
//Now we are going to check all current PlayerScores and see if this NetworkId has been used before
96
//If it has we set it to our new PlayerScoreComponent
97
bool uniqueNetworkId = true;
98
for (int i = 0; i < currentPlayerScoreEntities.Length; i++)
99
{
100
//We call the data componentData just to make it more legible on the if() line
101
var componentData = playerScoreComponent[currentPlayerScoreEntities[i]];
102
if(componentData.networkId == newPlayersNetworkId)
103
{
104
commandBuffer.SetComponent<PlayerScoreComponent>(currentPlayerScoreEntities[i], newPlayerScore);
105
uniqueNetworkId = false;
106
}
107
108
}
109
//If this NetworkId has not been used before we create a new PlayerScore
110
if (uniqueNetworkId)
111
{
112
var playerScoreEntity = commandBuffer.Instantiate(scorePrefab);
113
//We set the initial player score to 1 so the first player will be assigned this PlayerScore
114
commandBuffer.SetComponent<PlayerScoreComponent>(playerScoreEntity, newPlayerScore);
115
}
116
117
}).Schedule();
118
}
119
}
Copied!
Creating SetupScoreSystem
    Let's get back to NavigationScene, hit Play, click Host a Game, and then go to Entity Debugger
    Check out the ClientWorld (by selecting the drop down menu in the top left that currently has "Default World" selected, then choose ClientWorld)
      Find our PlayerScore and HighestScore components
Checking out Entity Debugger to see PlayerScore and HighestScore appearing on the client
    Great! We see that PlayerScore and HighestScore were created and that PlayerScore is updated to the players name
We have set up scoring
    We created HighestScoreComponent
    We created HighestScore prefab
    We created PlayerScoreComponent
    We created PlayerScoreAuthoringComponent
    We created PlayerScore prefab
    We created SendServerPlayerNameRpc
    We updated ClientLoadGameSystem
    We created SetupScoreSystem

Getting the Server to update scores

We are going to use a similar approach for updating scores as the one we used in ChangeMaterialAndDestroySystem when we added DestroyTags to entities bullets that had collisions.
When we have an OnEnter collision we will do analysis of who the bullet owner is and what it collided with. Based on the results we will update the bullet owner's PlayerScore and possibly the HighestScore.
    Let's create AdjustScoresFromBulletCollisionsSystem in the Server/Systems folder
    Paste the code snippet below into AdjustScoresFromBulletCollisionsSystem.cs:
1
using Unity.Collections;
2
using Unity.Entities;
3
using Unity.Jobs;
4
using Unity.Physics.Stateful;
5
using UnityEngine;
6
using Unity.NetCode;
7
8
9
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
10
public class AdjustPlayerScoresFromBulletCollisionSystem : SystemBase
11
{
12
private EndSimulationEntityCommandBufferSystem m_CommandBufferSystem;
13
private TriggerEventConversionSystem m_TriggerSystem;
14
private EntityQueryMask m_NonTriggerMask;
15
private EntityQuery m_PlayerScores;
16
private EntityQuery m_HighestScore;
17
18
protected override void OnCreate()
19
{
20
m_CommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
21
m_TriggerSystem = World.GetOrCreateSystem<TriggerEventConversionSystem>();
22
23
m_NonTriggerMask = EntityManager.GetEntityQueryMask(
24
GetEntityQuery(new EntityQueryDesc
25
{
26
None = new ComponentType[]
27
{
28
typeof(StatefulTriggerEvent)
29
}
30
})
31
);
32
//We set our queries
33
m_PlayerScores = GetEntityQuery(ComponentType.ReadWrite<PlayerScoreComponent>());
34
m_HighestScore = GetEntityQuery(ComponentType.ReadWrite<HighestScoreComponent>());
35
//We wait to update until we have our converted entities
36
RequireForUpdate(m_PlayerScores);
37
RequireForUpdate(m_HighestScore);
38
}
39
40
protected override void OnUpdate()
41
{
42
// Need this extra variable here so that it can
43
// be captured by Entities.ForEach loop below
44
var nonTriggerMask = m_NonTriggerMask;
45
46
//We grab all the player scores because we don't know who will need to be assigned points
47
var playerScoreEntities = m_PlayerScores.ToEntityArray(Allocator.TempJob);
48
//we will need to grab the PlayerScoreComponent from our player score entities to compare values
49
var playerScoreComponent = GetComponentDataFromEntity<PlayerScoreComponent>();
50
51
//We grab the 1 HighestScore engity
52
var highestScoreEntities = m_HighestScore.ToEntityArray(Allocator.TempJob);
53
//We will need to grab the HighestScoreComponent from our highest score entity to compare values
54
var highestScoreComponent = GetComponentDataFromEntity<HighestScoreComponent>();
55
56
//We are going to use this to pull the GhostOwnerComponent from the bullets to see who they belong to
57
var ghostOwner = GetComponentDataFromEntity<GhostOwnerComponent>();
58
59
//We need to dispose our entities
60
Entities
61
.WithDisposeOnCompletion(playerScoreEntities)
62
.WithDisposeOnCompletion(highestScoreEntities)
63
.WithName("ChangeMaterialOnTriggerEnter")
64
.ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> triggerEventBuffer) =>
65
{
66
for (int i = 0; i < triggerEventBuffer.Length; i++)
67
{
68
//Here we grab our bullet entity and the other entity it collided with
69
var triggerEvent = triggerEventBuffer[i];
70
var otherEntity = triggerEvent.GetOtherEntity(e);
71
72
// exclude other triggers and processed events
73
if (triggerEvent.State == EventOverlapState.Stay || !nonTriggerMask.Matches(otherEntity))
74
{
75
continue;
76
}
77
78
//We want our code to run on the first intersection of Bullet and other entity
79
else if (triggerEvent.State == EventOverlapState.Enter)
80
{
81
82
//We grab the NetworkId of the bullet so we know who to assign points to
83
var bulletsPlayerNetworkId = ghostOwner[e].NetworkId;
84
85
//We start with 0 points to add
86
int pointsToAdd = 0;
87
if (HasComponent<PlayerTag>(otherEntity))
88
{
89
//Now we check if the bullet came from the same player
90
if (ghostOwner[otherEntity].NetworkId == bulletsPlayerNetworkId)
91
{
92
//If it is from the same player no points
93
return;
94
}
95
pointsToAdd += 10;
96
}
97
98
if (HasComponent<AsteroidTag>(otherEntity))
99
{
100
//Bullet hitting an Asteroid is 1 point
101
pointsToAdd += 1;
102
}
103
104
//After updating the points to add we check the PlayerScore entities and find the one with the
105
//correct NetworkId so we can update the scores for the PlayerScoreComponent
106
//If the updated score is higher than the highest score it updates the highest score
107
for (int j = 0; j < playerScoreEntities.Length; j++)
108
{
109
//Grab the PlayerScore
110
var currentPlayScoreComponent = playerScoreComponent[playerScoreEntities[j]];
111
if(currentPlayScoreComponent.networkId == bulletsPlayerNetworkId)
112
{
113
//We create a new component with updated values
114
var newPlayerScore = new PlayerScoreComponent{
115
networkId = currentPlayScoreComponent.networkId,
116
playerName = currentPlayScoreComponent.playerName,
117
currentScore = currentPlayScoreComponent.currentScore + pointsToAdd,
118
highScore = currentPlayScoreComponent.highScore
119
};
120
//Here we check if the player beat their own high score
121
if (newPlayerScore.currentScore > newPlayerScore.highScore)
122
{
123
newPlayerScore.highScore = newPlayerScore.currentScore;
124
}
125
126
//Here we check if the player beat the highest score
127
var currentHighScore = highestScoreComponent[highestScoreEntities[0]];
128
if (newPlayerScore.highScore > currentHighScore.highestScore)
129
{
130
//If it does we make a new HighestScoreComponent
131
var updatedHighestScore = new HighestScoreComponent {
132
highestScore = newPlayerScore.highScore,
133
playerName = newPlayerScore.playerName
134
};
135
136
//The reason why we don't go with:
137
//SetComponent<HighestScoreComponent>(highestScoreEntities[0], updatedHighestScore);
138
//is because SetComponent<HighestScoreComponent>() gets codegen'd into ComponentDataFromEntity<HighestScoreComponent>()
139
//and you can't use 2 different ones or else you get an 'two containers may not be the same (aliasing)' error
140
highestScoreComponent[highestScoreEntities[0]] = updatedHighestScore;
141
}
142
// SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
143
//The reason why we don't go with:
144
//SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
145
//is because SetComponent<PlayerScoreComponent>() gets codegen'd into ComponentDataFromEntity<PlayerScoreComponent>()
146
//and you can't use 2 different ones or else you get an 'two containers may not be the same (aliasing)' error
147
playerScoreComponent[playerScoreEntities[j]] = newPlayerScore;
148
}
149
}
150
}
151
else
152
{
153
continue;
154
}
155
}
156
}).Schedule();
157
}
158
}
Copied!
Creating AdjustScoresFromBulletCollisionsSystem
    Okay once saved, let's hit play, host game, shoot around to destroy a couple asteroids, then head back to Entity Debugger to check out ClientWorld again to make sure that our PlayerScore and HighestScore are updating
Shooting around and PlayerScore updating with PlayerName and Current/High score
Seeing that the HighestScore entity's HighestScoreComponent has been updated
    Great! They are updating
    Now let's hit "p" to self-destruct
    Checkout the PlayerScore
Self-destructing does not cause the current score to go to 0
    Uh oh, the CurrentScore didn't go to 0 even though we self-destructed as a player
    Let's update PlayerDestructionSystem to also reset the players score to 0
    Paste the code snippet below into PlayerDestructionSystem.cs:
1
using Unity.Entities;
2
using Unity.Jobs;
3
using Unity.NetCode;
4
using Unity.Collections;
5
6
//We are going to update LATE once all other systems are complete
7
//because we don't want to destroy the Entity before other systems have
8
//had a chance to interact with it if they need to
9
[UpdateInWorld(UpdateInWorld.TargetWorld.Server)]
10
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
11
public class PlayerDestructionSystem : SystemBase
12
{
13
private EndSimulationEntityCommandBufferSystem m_EndSimEcb;
14
15
private EntityQuery m_PlayerScores;
16
private EntityQuery m_HighestScore;
17
18
protected override void OnCreate()
19
{
20
//We grab the EndSimulationEntityCommandBufferSystem to record our structural changes
21
m_EndSimEcb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
22
23
//We set our queries
24
m_PlayerScores = GetEntityQuery(ComponentType.ReadWrite<PlayerScoreComponent>());
25
m_HighestScore = GetEntityQuery(ComponentType.ReadWrite<HighestScoreComponent>());
26
}
27
28
protected override void OnUpdate()
29
{
30
//We add "AsParallelWriter" when we create our command buffer because we want
31
//to run our jobs in parallel
32
var commandBuffer = m_EndSimEcb.CreateCommandBuffer().AsParallelWriter();
33
34
//We are going to need to update the NCE CommandTargetComponent so we set the argument to false (not read-only)
35
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
36
37
JobHandle playerScoresDep;
38
//We grab all the player scores because we don't know who will need to be assigned points
39
var playerScoreEntities = m_PlayerScores.ToEntityArrayAsync(Allocator.TempJob, out playerScoresDep);
40
//we will need to grab the PlayerScoreComponent from our player score entities to compare values
41
var playerScoreComponent = GetComponentDataFromEntity<PlayerScoreComponent>();
42
43
44
//We now any entities with a DestroyTag and an PlayerTag
45
//We could just query for a DestroyTag, but we might want to run different processes
46
//if different entities are destroyed, so we made this one specifically for Players
47
//We query specifically for players because we need to clear the NCE when they are destroyed
48
//In order to write over a variable that we pass through to a job we must include "WithNativeDisableParallelForRestricion"
49
//It means "yes we know what we are doing, allow us to write over this variable"
50
var playerDestructionJob = Entities
51
.WithDisposeOnCompletion(playerScoreEntities)
52
.WithReadOnly(playerScoreEntities)
53
.WithNativeDisableParallelForRestriction(playerScoreComponent)
54
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
55
.WithAll<DestroyTag, PlayerTag>()
56
.ForEach((Entity entity, int nativeThreadIndex, in PlayerEntityComponent playerEntity, in GhostOwnerComponent ghostOwnerComponent) =>
57
{
58
// Reset the CommandTargetComponent on the Network Connection Entity to the player
59
//We are able to find the NCE the player belongs to through the PlayerEntity component
60
var state = commandTargetFromEntity[playerEntity.PlayerEntity];
61
state.targetEntity = Entity.Null;
62
commandTargetFromEntity[playerEntity.PlayerEntity] = state;
63
64
//Now we cycle through PlayerScores till we find the right onw
65
for (int j = 0; j < playerScoreEntities.Length; j++)
66
{
67
//Grab the PlayerScore
68
var currentPlayScoreComponent = playerScoreComponent[playerScoreEntities[j]];
69
//Check if the player to destroy has the same NetworkId as the current PlayerScore
70
if(currentPlayScoreComponent.networkId == ghostOwnerComponent.NetworkId)
71
{
72
//We create a new component with updated values
73
var newPlayerScore = new PlayerScoreComponent{
74
networkId = currentPlayScoreComponent.networkId,
75
playerName = currentPlayScoreComponent.playerName,
76
currentScore = 0,
77
highScore = currentPlayScoreComponent.highScore
78
};
79
// SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
80
//The reason why we don't go with:
81
//SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
82
//is because SetComponent<PlayerScoreComponent>() gets codegen'd into ComponentDataFromEntity<PlayerScoreComponent>()
83
//and you can't use 2 different ones or else you get an 'two containers may not be the same (aliasing)' error
84
playerScoreComponent[playerScoreEntities[j]] = newPlayerScore;
85
}
86
}
87
//Then destroy the entity
88
commandBuffer.DestroyEntity(nativeThreadIndex, entity);
89
90
}).ScheduleParallel(JobHandle.CombineDependencies(Dependency, playerScoresDep));
91
92
//We set the system dependency
93
Dependency = playerDestructionJob;
94
//We then add the dependencies of these jobs to the EndSimulationEntityCOmmandBufferSystem
95
//that will be playing back the structural changes recorded in this sytem
96
m_EndSimEcb.AddJobHandleForProducer(Dependency);
97
98
}
99
}
Copied!
Updating PlayerDestructionSystem
    Once that's updated, hit Play, host game, shoot around again, destroy some asteroids, hit "p" to self-destruct and checkout our PlayerScoreComponent
We now have our scores updating based on bullet collisions
    We created AdjustScoresFromBulletCollisionsSystem
    We updated PlayerDestructionSystem

Client updating their Game UI

Now that we have our ghosted PlayerScores and HighestScore our client can access them to update their Game UI.
The server calls the shots so the logic we use will be simple. If your UI values do not equal ghost values, change your UI values to the ghost values.
Let's update GameOverlayUpdater to including querying for the PlayerScores and HighestScore.
    Paste the code snippet below into GameOverlayUpdater.cs:
1
using UnityEngine;
2
using UnityEngine.UIElements;
3
using Unity.Entities;
4
using Unity.NetCode;
5
using Unity.Collections;
6
public class GameOverlayUpdater : MonoBehaviour
7
{
8
//This is how we will grab access to the UI elements we need to update
9
public UIDocument m_GameUIDocument;
10
private VisualElement m_GameManagerUIVE;
11
private Label m_GameName;
12
private Label m_GameIp;
13
private Label m_PlayerName;
14
private Label m_CurrentScoreText;
15
private Label m_HighScoreText;
16
private Label m_HighestScoreText;
17
//We will need ClientServerInfo to update our VisualElements with appropriate values
18
public ClientServerInfo ClientServerInfo;
19
private World m_ClientWorld;
20
private ClientSimulationSystemGroup m_ClientWorldSimulationSystemGroup;
21
//Will check for GameNameComponent
22
private EntityQuery m_GameNameComponentQuery;
23
private bool gameNameIsSet = false;
24
//We need the PlayerScores and HighestScore as well as our NetworkId
25
//We are going to set our NetworkId and then query the ghosts for the PlayerScore entity associated with us
26
private EntityQuery m_NetworkConnectionEntityQuery;
27
private EntityQuery m_PlayerScoresQuery;
28
private EntityQuery m_HighestScoreQuery;
29
private bool networkIdIsSet = false;
30
private int m_NetworkId;
31
private Entity ClientPlayerScoreEntity;
32
public int m_CurrentScore;
33
public int m_HighScore;
34
public int m_HighestScore;
35
public string m_HighestScoreName;
36
void OnEnable()
37
{
38
//We set the labels that we will need to update
39
m_GameManagerUIVE = m_GameUIDocument.rootVisualElement;
40
m_GameName = m_GameManagerUIVE.Q<Label>("game-name");
41
m_GameIp = m_GameManagerUIVE.Q<Label>("game-ip");
42
m_PlayerName = m_GameManagerUIVE.Q<Label>("player-name");
43
//Scores will be updated in a future section
44
m_CurrentScoreText = m_GameManagerUIVE.Q<Label>("current-score");
45
m_HighScoreText = m_GameManagerUIVE.Q<Label>("high-score");
46
m_HighestScoreText = m_GameManagerUIVE.Q<Label>("highest-score");
47
}
48
// Start is called before the first frame update
49
void Start()
50
{
51
//We set the initial client data we already have as part of ClientDataComponent
52
m_GameIp.text = ClientServerInfo.ConnectToServerIp;
53
m_PlayerName.text = ClientServerInfo.PlayerName;
54
//If it is not the client, stop running this script (unnecessary)
55
if (!ClientServerInfo.IsClient)
56
{
57
this.enabled = false;
58
}
59
//Now we search for the client world and the client simulation system group
60
//so we can communicated with ECS in this MonoBehaviour
61
foreach (var world in World.All)
62
{
63
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
64
{
65
m_ClientWorld = world;
66
m_ClientWorldSimulationSystemGroup = world.GetExistingSystem<ClientSimulationSystemGroup>();
67
m_GameNameComponentQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<GameNameComponent>());
68
//Grabbing the queries we need for updating scores
69
m_NetworkConnectionEntityQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<NetworkIdComponent>());
70
m_PlayerScoresQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<PlayerScoreComponent>());
71
m_HighestScoreQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<HighestScoreComponent>());
72
}
73
}
74
}
75
// Update is called once per frame
76
void Update()
77
{
78
//We do not need to continue if we do not have a GameNameComponent yet
79
if(m_GameNameComponentQuery.IsEmptyIgnoreFilter)
80
return;
81
//If we have a GameNameComponent we need to update ClientServerInfo and then our UI
82
//We only need to do this once so we have a boolean flag to prevent this from being ran more than once
83
if(!gameNameIsSet)
84
{
85
ClientServerInfo.GameName = m_ClientWorldSimulationSystemGroup.GetSingleton<GameNameComponent>().GameName.ToString();
86
m_GameName.text = ClientServerInfo.GameName;
87
gameNameIsSet = true;
88
}
89
//Now we will handle updating scoring
90
//We check if the scoring entities exist, otherwise why bother
91
if(m_NetworkConnectionEntityQuery.IsEmptyIgnoreFilter || m_PlayerScoresQuery.IsEmptyIgnoreFilter || m_HighestScoreQuery.IsEmptyIgnoreFilter)
92
return;
93
//We set our NetworkId once
94
if(!networkIdIsSet)
95
{
96
m_NetworkId = m_ClientWorldSimulationSystemGroup.GetSingleton<NetworkIdComponent>().Value;
97
networkIdIsSet = true;
98
}
99
//Grab PlayerScore entities
100
var playerScoresNative = m_PlayerScoresQuery.ToEntityArray(Allocator.TempJob);
101
//For each entity find the entity with a matching NetworkId
102
for (int j = 0; j < playerScoresNative.Length; j++)
103
{
104
//Grab the NetworkId of the PlayerScore entity
105
var netId = m_ClientWorldSimulationSystemGroup.GetComponentDataFromEntity<PlayerScoreComponent>(true)[playerScoresNative[j]].networkId;
106
//Check if it matches our NetworkId that we set
107
if(netId == m_NetworkId)
108
{
109
//If it matches set our ClientPlayerScoreEntity
110
ClientPlayerScoreEntity = playerScoresNative[j];
111
}
112
}
113
//No need for this anymore
114
playerScoresNative.Dispose();
115
//Every Update() we get grab the PlayerScoreComponent from our set Entity and check it out with current values
116
var playerScoreComponent = m_ClientWorldSimulationSystemGroup.GetComponentDataFromEntity<PlayerScoreComponent>(true)[ClientPlayerScoreEntity];
117
//Check if current is different and update to ghost value
118
if(m_CurrentScore != playerScoreComponent.currentScore)
119
{
120
//If it is make it match the ghost value
121
m_CurrentScore = playerScoreComponent.currentScore;
122
UpdateCurrentScore();
123
}
124
//Check if current is different and update to ghost value
125
if(m_HighScore != playerScoreComponent.highScore)
126
{
127
//If it is make it match the ghost value
128
m_HighScore = playerScoreComponent.highScore;
129
UpdateHighScore();
130
}
131
//We grab our HighestScoreComponent
132
var highestScoreNative = m_HighestScoreQuery.ToComponentDataArray<HighestScoreComponent>(Allocator.TempJob);
133
//We check if its current value is different than ghost value
134
if(highestScoreNative[0].highestScore != m_HighestScore)
135
{
136
//If it is make it match the ghost value
137
m_HighestScore = highestScoreNative[0].highestScore;
138
m_HighestScoreName = highestScoreNative[0].playerName.ToString();
139
UpdateHighestScore();
140
}
141
highestScoreNative.Dispose();
142
}
143
void UpdateCurrentScore()
144
{
145
m_CurrentScoreText.text = m_CurrentScore.ToString();
146
}
147
void UpdateHighScore()
148
{
149
m_HighScoreText.text = m_HighScore.ToString();
150
}
151
void UpdateHighestScore()
152
{
153
m_HighestScoreText.text = m_HighestScoreName.ToString() + " - " + m_HighestScore.ToString();
154
}
155
}
Copied!
Updating GameOverlayUpdater
    Now hit play, host game, and shoot around at the asteroids and see your score increase
    Hit "p" to self-destruct and start again and see the score reset to 0
Game UI updating when player shoots asteroids
Our game UI updates based on updated ghost values
    We updated GameOverlayUpdater
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Score-Keeping'
Last modified 7mo ago