Keep Score and Update Game UI
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.

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:
using Unity.Networking.Transport;
using Unity.NetCode;
using Unity.Burst;
using Unity.Entities;
using Unity.Collections;
[GenerateAuthoringComponent]
public struct HighestScoreComponent : IComponentData
{
[GhostField]
public FixedString64Bytes playerName;
[GhostField]
public int highestScore;
}
  • 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:
using Unity.Entities;
[GenerateAuthoringComponent]
public struct PlayerScoreAuthoringComponent : IComponentData
{
public Entity Prefab;
}
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:
using Unity.Networking.Transport;
using Unity.NetCode;
using Unity.Burst;
using Unity.Entities;
using Unity.Collections;
[GenerateAuthoringComponent]
public struct PlayerScoreComponent : IComponentData
{
[GhostField]
public int networkId;
[GhostField]
public FixedString64Bytes playerName;
[GhostField]
public int currentScore;
[GhostField]
public int highScore;
}
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:
using AOT;
using Unity.Burst;
using Unity.Networking.Transport;
using Unity.NetCode;
using Unity.Entities;
using Unity.Collections;
using System.Collections;
using System;
public struct SendServerPlayerNameRpc : IRpcCommand
{
public FixedString64Bytes playerName;
}
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:
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
//This will only run on the client because it updates in ClientSimulationSystemGroup (which the server does not have)
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
[UpdateBefore(typeof(RpcSystem))]
public partial class ClientLoadGameSystem : SystemBase
{
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
protected override void OnCreate()
{
//We will be using the BeginSimECB
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//Requiring the ReceiveRpcCommandRequestComponent ensures that update is only run when an NCE exists and a SendClientGameRpc has come in
RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly<SendClientGameRpc>(), ComponentType.ReadOnly<ReceiveRpcCommandRequestComponent>()));
//This is just here to make sure the Sub Scene is streamed in before the client sets up the level data
RequireSingletonForUpdate<GameSettingsComponent>();
//We will make sure we have our ClientDataComponent so we can send the server our player name
RequireSingletonForUpdate<ClientDataComponent>();
}
protected override void OnUpdate()
{
//We must declare our local variables before using them within a job (.ForEach)
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
var gameSettingsEntity = GetSingletonEntity<GameSettingsComponent>();
var getGameSettingsComponentData = GetComponentDataFromEntity<GameSettingsComponent>();
var clientData = GetSingleton<ClientDataComponent>(); //We will use this to send the player name to server
Entities
.ForEach((Entity entity, in SendClientGameRpc request, in ReceiveRpcCommandRequestComponent requestSource) =>
{
//This destroys the incoming RPC so the code is only run once
commandBuffer.DestroyEntity(entity);
//Check for disconnects before moving forward
if (!rpcFromEntity.HasComponent(requestSource.SourceConnection))
return;
//Set the game size (unnecessary right now but we are including it to show how it is done)
getGameSettingsComponentData[gameSettingsEntity] = new GameSettingsComponent
{
levelWidth = request.levelWidth,
levelHeight = request.levelHeight,
levelDepth = request.levelDepth,
playerForce = request.playerForce,
bulletVelocity = request.bulletVelocity
};
//Here we create a new singleton entity for GameNameComponent
//We could add this component to the singleton entity that has the GameSettingsComponent
//but we will keep them separate in case we want to change workflows in the future and don't
//want these components to be dependent on the same entity
var gameNameEntity= commandBuffer.CreateEntity();
commandBuffer.AddComponent(gameNameEntity, new GameNameComponent {
GameName = request.gameName
});
//These update the NCE with NetworkStreamInGame (required to start receiving snapshots) and
//PlayerSpawningStateComponent, which we will use when we spawn players
commandBuffer.AddComponent(requestSource.SourceConnection, new PlayerSpawningStateComponent());
commandBuffer.AddComponent(requestSource.SourceConnection, default(NetworkStreamInGame));
//This tells the server "I loaded the level"
//First we create an entity called levelReq that will have 2 necessary components
//Next we add the RPC we want to send (SendServerGameLoadedRpc) and then we add
//SendRpcCommandRequestComponent with our TargetConnection being the NCE with the server (which will send it to the server)
var levelReq = commandBuffer.CreateEntity();
commandBuffer.AddComponent(levelReq, new SendServerGameLoadedRpc());
commandBuffer.AddComponent(levelReq, new SendRpcCommandRequestComponent {TargetConnection = requestSource.SourceConnection});
// this tells the server "This is my name and Id" which will be used for player score tracking
var playerReq = commandBuffer.CreateEntity();
commandBuffer.AddComponent(playerReq, new SendServerPlayerNameRpc {playerName = clientData.PlayerName});
commandBuffer.AddComponent(playerReq, new SendRpcCommandRequestComponent {TargetConnection = requestSource.SourceConnection});
}).Schedule();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
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:
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
using Unity.Burst;
using Unity.Jobs;
//The server will be keeping score
//The client will only read the scores and update overlay
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public partial class SetupScoreSystem : SystemBase
{
//We will be making structural changes so we need a command buffer
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//This will be the query for the highest score
private EntityQuery m_HighestScoreQuery;
//This will be the query for the player scores
private EntityQuery m_PlayerScoresQuery;
//This will be the prefab used to create PlayerScores
private Entity m_Prefab;
protected override void OnCreate()
{
//We set the command buffer
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//This will be used to check if there is already HighestScore (initialization)
m_HighestScoreQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly<HighestScoreComponent>());
//This will be used to check if there are already PlayerScores (initialization)
m_PlayerScoresQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly<PlayerScoreComponent>());
//We are going to wait to initialize and update until the first player connects and sends their name
RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly<SendServerPlayerNameRpc>(), ComponentType.ReadOnly<ReceiveRpcCommandRequestComponent>()));
}
protected override void OnUpdate()
{
//Here we set the prefab we will use
if (m_Prefab == Entity.Null)
{
//We grab the converted PrefabCollection Entity's PlayerScoreAuthoringComponent
//and set m_Prefab to its Prefab value
m_Prefab = GetSingleton<PlayerScoreAuthoringComponent>().Prefab;
//We then initialize by creating the first PlayerScore
var initialPlayerScore = EntityManager.Instantiate(m_Prefab);
//We set the initial player score to 1 so the first player will be assigned this PlayerScore
EntityManager.SetComponentData<PlayerScoreComponent>(initialPlayerScore, new PlayerScoreComponent{
networkId = 1
});
//we must "return" after setting this prefab because if we were to continue into the Job
//we would run into errors because the variable was JUST set (ECS funny business)
//comment out return and see the error
return;
}
//We need to declare our local variables before the .ForEach()
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
//We use this to check for disconnects
var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
//We are going to grab all existing player scores because we need to check if the new player has an old NetworkId
var currentPlayerScoreEntities = m_PlayerScoresQuery.ToEntityArray(Allocator.TempJob);
//We are going to need to grab the Player score from the entity
var playerScoreComponent = GetComponentDataFromEntity<PlayerScoreComponent>();
//We grab the prefab in case we need to create a new PlayerScore for a new NetworkId
var scorePrefab = m_Prefab;
//We are going to need to be able to grab the NetworkIdComponent from the RPC source to know what the player's NetworkId is
var networkIdFromEntity = GetComponentDataFromEntity<NetworkIdComponent>();
Entities
.WithDisposeOnCompletion(currentPlayerScoreEntities)
.ForEach((Entity entity, in SendServerPlayerNameRpc request, in ReceiveRpcCommandRequestComponent requestSource) =>
{
//Delete the rpc
commandBuffer.DestroyEntity(entity);
//Check for disconnects
if (!rpcFromEntity.HasComponent(requestSource.SourceConnection))
return;
//Grab the NetworkIdComponent's Value
var newPlayersNetworkId = networkIdFromEntity[requestSource.SourceConnection].Value;
//We create a clean PlayerScore component with the player's name and the player's NetworkId value
var newPlayerScore = new PlayerScoreComponent{
networkId = newPlayersNetworkId,
playerName = request.playerName,
currentScore = 0,
highScore = 0
};
//Now we are going to check all current PlayerScores and see if this NetworkId has been used before
//If it has we set it to our new PlayerScoreComponent
bool uniqueNetworkId = true;
for (int i = 0; i < currentPlayerScoreEntities.Length; i++)
{
//We call the data componentData just to make it more legible on the if() line
var componentData = playerScoreComponent[currentPlayerScoreEntities[i]];
if(componentData.networkId == newPlayersNetworkId)
{
commandBuffer.SetComponent<PlayerScoreComponent>(currentPlayerScoreEntities[i], newPlayerScore);
uniqueNetworkId = false;
}
}
//If this NetworkId has not been used before we create a new PlayerScore
if (uniqueNetworkId)
{
var playerScoreEntity = commandBuffer.Instantiate(scorePrefab);
//We set the initial player score to 1 so the first player will be assigned this PlayerScore
commandBuffer.SetComponent<PlayerScoreComponent>(playerScoreEntity, newPlayerScore);
}
}).Schedule();
}
}
Creating SetupScoreSystem
  • Let's get back to NavigationScene, hit Play, click Host a Game, and then go to DOTS Windows
  • 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:
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics.Stateful;
using UnityEngine;
using Unity.NetCode;
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public partial class AdjustPlayerScoresFromBulletCollisionSystem : SystemBase
{
private EndSimulationEntityCommandBufferSystem m_CommandBufferSystem;
private StatefulTriggerEventBufferSystem m_TriggerSystem;
private EntityQueryMask m_NonTriggerMask;
private EntityQuery m_PlayerScores;
private EntityQuery m_HighestScore;
protected override void OnCreate()
{
m_CommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
m_TriggerSystem = World.GetOrCreateSystem<StatefulTriggerEventBufferSystem>();
m_NonTriggerMask = EntityManager.GetEntityQueryMask(
GetEntityQuery(new EntityQueryDesc
{
None = new ComponentType[]
{
typeof(StatefulTriggerEvent)
}
})
);
//We set our queries
m_PlayerScores = GetEntityQuery(ComponentType.ReadWrite<PlayerScoreComponent>());
m_HighestScore = GetEntityQuery(ComponentType.ReadWrite<HighestScoreComponent>());
//We wait to update until we have our converted entities
RequireForUpdate(m_PlayerScores);
RequireForUpdate(m_HighestScore);
}
protected override void OnUpdate()
{
// Need this extra variable here so that it can
// be captured by Entities.ForEach loop below
var nonTriggerMask = m_NonTriggerMask;
//We grab all the player scores because we don't know who will need to be assigned points
var playerScoreEntities = m_PlayerScores.ToEntityArray(Allocator.TempJob);
//we will need to grab the PlayerScoreComponent from our player score entities to compare values
var playerScoreComponent = GetComponentDataFromEntity<PlayerScoreComponent>();
//We grab the 1 HighestScore engity
var highestScoreEntities = m_HighestScore.ToEntityArray(Allocator.TempJob);
//We will need to grab the HighestScoreComponent from our highest score entity to compare values
var highestScoreComponent = GetComponentDataFromEntity<HighestScoreComponent>();
//We are going to use this to pull the GhostOwnerComponent from the bullets to see who they belong to
var ghostOwner = GetComponentDataFromEntity<GhostOwnerComponent>();
//We need to dispose our entities
Entities
.WithDisposeOnCompletion(playerScoreEntities)
.WithDisposeOnCompletion(highestScoreEntities)
.WithName("ChangeMaterialOnTriggerEnter")
.ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> triggerEventBuffer) =>
{
for (int i = 0; i < triggerEventBuffer.Length; i++)
{
//Here we grab our bullet entity and the other entity it collided with
var triggerEvent = triggerEventBuffer[i];
var otherEntity = triggerEvent.GetOtherEntity(e);
// exclude other triggers and processed events
if (triggerEvent.State == StatefulEventState.Stay || !nonTriggerMask.Matches(otherEntity))
{
continue;
}
//We want our code to run on the first intersection of Bullet and other entity
else if (triggerEvent.State == StatefulEventState.Enter)
{
//We grab the NetworkId of the bullet so we know who to assign points to
var bulletsPlayerNetworkId = ghostOwner[e].NetworkId;
//We start with 0 points to add
int pointsToAdd = 0;
if (HasComponent<PlayerTag>(otherEntity))
{
//Now we check if the bullet came from the same player
if (ghostOwner[otherEntity].NetworkId == bulletsPlayerNetworkId)
{
//If it is from the same player no points
return;
}
pointsToAdd += 10;
}
if (HasComponent<AsteroidTag>(otherEntity))
{
//Bullet hitting an Asteroid is 1 point
pointsToAdd += 1;
}
//After updating the points to add we check the PlayerScore entities and find the one with the
//correct NetworkId so we can update the scores for the PlayerScoreComponent
//If the updated score is higher than the highest score it updates the highest score
for (int j = 0; j < playerScoreEntities.Length; j++)
{
//Grab the PlayerScore
var currentPlayScoreComponent = playerScoreComponent[playerScoreEntities[j]];
if(currentPlayScoreComponent.networkId == bulletsPlayerNetworkId)
{
//We create a new component with updated values
var newPlayerScore = new PlayerScoreComponent{
networkId = currentPlayScoreComponent.networkId,
playerName = currentPlayScoreComponent.playerName,
currentScore = currentPlayScoreComponent.currentScore + pointsToAdd,
highScore = currentPlayScoreComponent.highScore
};
//Here we check if the player beat their own high score
if (newPlayerScore.currentScore > newPlayerScore.highScore)
{
newPlayerScore.highScore = newPlayerScore.currentScore;
}
//Here we check if the player beat the highest score
var currentHighScore = highestScoreComponent[highestScoreEntities[0]];
if (newPlayerScore.highScore > currentHighScore.highestScore)
{
//If it does we make a new HighestScoreComponent
var updatedHighestScore = new HighestScoreComponent {
highestScore = newPlayerScore.highScore,
playerName = newPlayerScore.playerName
};
//The reason why we don't go with:
//SetComponent<HighestScoreComponent>(highestScoreEntities[0], updatedHighestScore);
//is because SetComponent<HighestScoreComponent>() gets codegen'd into ComponentDataFromEntity<HighestScoreComponent>()
//and you can't use 2 different ones or else you get an 'two containers may not be the same (aliasing)' error
highestScoreComponent[highestScoreEntities[0]] = updatedHighestScore;
}
// SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
//The reason why we don't go with:
//SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
//is because SetComponent<PlayerScoreComponent>() gets codegen'd into ComponentDataFromEntity<PlayerScoreComponent>()
//and you can't use 2 different ones or else you get an 'two containers may not be the same (aliasing)' error
playerScoreComponent[playerScoreEntities[j]] = newPlayerScore;
}
}
}
else
{
continue;
}
}
}).Schedule();
}
}
Creating AdjustScoresFromBulletCollisionsSystem
  • Okay once saved, let's hit play, host game, shoot around to destroy a couple asteroids, then head back to DOTS Windows 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
  • 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:
using Unity.Entities;
using Unity.Jobs;
using Unity.NetCode;
using Unity.Collections;
//We are going to update LATE once all other systems are complete
//because we don't want to destroy the Entity before other systems have
//had a chance to interact with it if they need to
[UpdateInWorld(TargetWorld.Server)]
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
public partial class PlayerDestructionSystem : SystemBase
{
private EndSimulationEntityCommandBufferSystem m_EndSimEcb;
private EntityQuery m_PlayerScores;
private EntityQuery m_HighestScore;
protected override void OnCreate()
{
//We grab the EndSimulationEntityCommandBufferSystem to record our structural changes
m_EndSimEcb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
//We set our queries
m_PlayerScores = GetEntityQuery(ComponentType.ReadWrite<PlayerScoreComponent>());
m_HighestScore = GetEntityQuery(ComponentType.ReadWrite<HighestScoreComponent>());
}
protected override void OnUpdate()
{
//We add "AsParallelWriter" when we create our command buffer because we want
//to run our jobs in parallel
var commandBuffer = m_EndSimEcb.CreateCommandBuffer().AsParallelWriter();
//We are going to need to update the NCE CommandTargetComponent so we set the argument to false (not read-only)
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
JobHandle playerScoresDep;
//We grab all the player scores because we don't know who will need to be assigned points
var playerScoreEntities = m_PlayerScores.ToEntityArrayAsync(Allocator.TempJob, out playerScoresDep);
//we will need to grab the PlayerScoreComponent from our player score entities to compare values
var playerScoreComponent = GetComponentDataFromEntity<PlayerScoreComponent>();
//We now any entities with a DestroyTag and an PlayerTag
//We could just query for a DestroyTag, but we might want to run different processes
//if different entities are destroyed, so we made this one specifically for Players
//We query specifically for players because we need to clear the NCE when they are destroyed
//In order to write over a variable that we pass through to a job we must include "WithNativeDisableParallelForRestricion"
//It means "yes we know what we are doing, allow us to write over this variable"
var playerDestructionJob = Entities
.WithDisposeOnCompletion(playerScoreEntities)
.WithReadOnly(playerScoreEntities)
.WithNativeDisableParallelForRestriction(playerScoreComponent)
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
.WithAll<DestroyTag, PlayerTag>()
.ForEach((Entity entity, int nativeThreadIndex, in PlayerEntityComponent playerEntity, in GhostOwnerComponent ghostOwnerComponent) =>
{
// Reset the CommandTargetComponent on the Network Connection Entity to the player
//We are able to find the NCE the player belongs to through the PlayerEntity component
var state = commandTargetFromEntity[playerEntity.PlayerEntity];
state.targetEntity = Entity.Null;
commandTargetFromEntity[playerEntity.PlayerEntity] = state;
//Now we cycle through PlayerScores till we find the right onw
for (int j = 0; j < playerScoreEntities.Length; j++)
{
//Grab the PlayerScore
var currentPlayScoreComponent = playerScoreComponent[playerScoreEntities[j]];
//Check if the player to destroy has the same NetworkId as the current PlayerScore
if(currentPlayScoreComponent.networkId == ghostOwnerComponent.NetworkId)
{
//We create a new component with updated values
var newPlayerScore = new PlayerScoreComponent{
networkId = currentPlayScoreComponent.networkId,
playerName = currentPlayScoreComponent.playerName,
currentScore = 0,
highScore = currentPlayScoreComponent.highScore
};
// SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
//The reason why we don't go with:
//SetComponent<PlayerScoreComponent>(playerScoreEntities[j], newPlayerScore);
//is because SetComponent<PlayerScoreComponent>() gets codegen'd into ComponentDataFromEntity<PlayerScoreComponent>()
//and you can't use 2 different ones or else you get an 'two containers may not be the same (aliasing)' error
playerScoreComponent[playerScoreEntities[j]] = newPlayerScore;
}
}
//Then destroy the entity
commandBuffer.DestroyEntity(nativeThreadIndex, entity);
}).ScheduleParallel(JobHandle.CombineDependencies(Dependency, playerScoresDep));
//We set the system dependency
Dependency = playerDestructionJob;
//We then add the dependencies of these jobs to the EndSimulationEntityCOmmandBufferSystem
//that will be playing back the structural changes recorded in this sytem
m_EndSimEcb.AddJobHandleForProducer(Dependency);
}
}
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

Update Game UI With UI Toolkit Data Binding

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:
using UnityEngine;
using UnityEngine.UIElements;
using Unity.Entities;
using Unity.NetCode;
using Unity.Collections;
public class GameOverlayUpdater : MonoBehaviour
{
//This is how we will grab access to the UI elements we need to update
public UIDocument m_GameUIDocument;
private VisualElement m_GameManagerUIVE;
private Label m_GameName;
private Label m_GameIp;
private Label m_PlayerName;
private Label m_CurrentScoreText;
private Label m_HighScoreText;
private Label m_HighestScoreText;
//We will need ClientServerInfo to update our VisualElements with appropriate values
public ClientServerInfo ClientServerInfo;
private World m_ClientWorld;
private ClientSimulationSystemGroup m_ClientWorldSimulationSystemGroup;
//Will check for GameNameComponent
private EntityQuery m_GameNameComponentQuery;
private bool gameNameIsSet = false;
//We need the PlayerScores and HighestScore as well as our NetworkId
//We are going to set our NetworkId and then query the ghosts for the PlayerScore entity associated with us
private EntityQuery m_NetworkConnectionEntityQuery;
private EntityQuery m_PlayerScoresQuery;
private EntityQuery m_HighestScoreQuery;
private bool networkIdIsSet = false;
private int m_NetworkId;
private Entity ClientPlayerScoreEntity;
public int m_CurrentScore;
public int m_HighScore;
public int m_HighestScore;
public string m_HighestScoreName;
void OnEnable()
{
//We set the labels that we will need to update
m_GameManagerUIVE = m_GameUIDocument.rootVisualElement;
m_GameName = m_GameManagerUIVE.Q<Label>("game-name");
m_GameIp = m_GameManagerUIVE.Q<Label>("game-ip");
m_PlayerName = m_GameManagerUIVE.Q<Label>("player-name");
//Scores will be updated in a future section
m_CurrentScoreText = m_GameManagerUIVE.Q<Label>("current-score");
m_HighScoreText = m_GameManagerUIVE.Q<Label>("high-score");
m_HighestScoreText = m_GameManagerUIVE.Q<Label>("highest-score");
}
// Start is called before the first frame update
void Start()
{
//We set the initial client data we already have as part of ClientDataComponent
m_GameIp.text = ClientServerInfo.ConnectToServerIp;
m_PlayerName.text = ClientServerInfo.PlayerName;
//If it is not the client, stop running this script (unnecessary)
if (!ClientServerInfo.IsClient)
{
this.enabled = false;
}
//Now we search for the client world and the client simulation system group
//so we can communicated with ECS in this MonoBehaviour
foreach (var world in World.All)
{
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
{
m_ClientWorld = world;
m_ClientWorldSimulationSystemGroup = world.GetExistingSystem<ClientSimulationSystemGroup>();
m_GameNameComponentQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<GameNameComponent>());
//Grabbing the queries we need for updating scores
m_NetworkConnectionEntityQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<NetworkIdComponent>());
m_PlayerScoresQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<PlayerScoreComponent>());
m_HighestScoreQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<HighestScoreComponent>());
}
}
}
// Update is called once per frame
void Update()
{
//We do not need to continue if we do not have a GameNameComponent yet
if(m_GameNameComponentQuery.IsEmptyIgnoreFilter)
return;
//If we have a GameNameComponent we need to update ClientServerInfo and then our UI
//We only need to do this once so we have a boolean flag to prevent this from being ran more than once
if(!gameNameIsSet)
{
ClientServerInfo.GameName = m_ClientWorldSimulationSystemGroup.GetSingleton<GameNameComponent>().GameName.ToString();
m_GameName.text = ClientServerInfo.GameName;
gameNameIsSet = true;
}
//Now we will handle updating scoring
//We check if the scoring entities exist, otherwise why bother
if(m_NetworkConnectionEntityQuery.IsEmptyIgnoreFilter || m_PlayerScoresQuery.IsEmptyIgnoreFilter || m_HighestScoreQuery.IsEmptyIgnoreFilter)
return;
//We set our NetworkId once
if(!networkIdIsSet)
{
m_NetworkId = m_ClientWorldSimulationSystemGroup.GetSingleton<NetworkIdComponent>().Value;
networkIdIsSet = true;
}
//Grab PlayerScore entities
var playerScoresNative = m_PlayerScoresQuery.ToEntityArray(Allocator.TempJob);
//For each entity find the entity with a matching NetworkId
for (int j = 0; j < playerScoresNative.Length; j++)
{
//Grab the NetworkId of the PlayerScore entity
var netId = m_ClientWorldSimulationSystemGroup.GetComponentDataFromEntity<PlayerScoreComponent>(true)[playerScoresNative[j]].networkId;
//Check if it matches our NetworkId that we set
if(netId == m_NetworkId)
{
//If it matches set our ClientPlayerScoreEntity
ClientPlayerScoreEntity = playerScoresNative[j];
}
}
//No need for this anymore
playerScoresNative.Dispose();
//Every Update() we get grab the PlayerScoreComponent from our set Entity and check it out with current values
var playerScoreComponent = m_ClientWorldSimulationSystemGroup.GetComponentDataFromEntity<PlayerScoreComponent>(true)[ClientPlayerScoreEntity];
//Check if current is different and update to ghost value
if(m_CurrentScore != playerScoreComponent.currentScore)
{
//If it is make it match the ghost value
m_CurrentScore = playerScoreComponent.currentScore;
UpdateCurrentScore();
}
//Check if current is different and update to ghost value
if(m_HighScore != playerScoreComponent.highScore)
{
//If it is make it match the ghost value
m_HighScore = playerScoreComponent.highScore;
UpdateHighScore();
}
//We grab our HighestScoreComponent
var highestScoreNative = m_HighestScoreQuery.ToComponentDataArray<HighestScoreComponent>(Allocator.TempJob);
//We check if its current value is different than ghost value
if(highestScoreNative[0].highestScore != m_HighestScore)
{
//If it is make it match the ghost value
m_HighestScore = highestScoreNative[0].highestScore;
m_HighestScoreName = highestScoreNative[0].playerName.ToString();
UpdateHighestScore();
}
highestScoreNative.Dispose();
}
void UpdateCurrentScore()
{
m_CurrentScoreText.text = m_CurrentScore.ToString();
}
void UpdateHighScore()
{
m_HighScoreText.text = m_HighScore.ToString();
}
void UpdateHighestScore()
{
m_HighestScoreText.text = m_HighestScoreName.ToString() + " - " + m_HighestScore.ToString();
}
}
Updating GameOverlayUpdater
  • Now hit play, host game, and shoot around at the asteroids and see your score increase
Warning! This might not work as expected if there are thin-clients, since the UI-Code might find the score entity of one of the thin clients instead of the actual player.
  • 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'
Copy link
On this page
What you'll develop on this page
How we'll be keeping score
Scoring
How the server will keep score
Recycling HighScore ghosts
Server creating scores
Getting the Server to update scores
Update Game UI With UI Toolkit Data Binding