Use DOTS NetCode for Collisions and Destroying Bullet Prefabs
Code and workflows to spawn ghosted bullets and update to server-side destruction

Spawning ghosted bullets to destroy asteroids and players while interacting with Thin Clients
We will "turn" our bullet prefabs "into" ghosts and update the entity destruction flow so it is server-authoritative.
Entity spawningWhen the client side receives a new ghost, the ghost type is determined by a set of classification systems and then a spawn system spawns it. There is no specific spawn message, and when the client receives an unknown ghost ID, it counts as an implicit spawn.Because the client interpolates snapshot data, Unity cannot spawn entities immediately, unless it was preemptively spawned, such as with spawn prediction. This is because the data is not ready for the client to interpolate it. Otherwise, the object would appear and then not get any more updates until the interpolation is ready.Therefore normal spawns happen in a delayed manner. Spawning is split into three main types as follows:
Delayed or interpolated spawning. The entity is spawned when the interpolation system is ready to apply updates. This is how remote entities are handled, because they are interpolated in a straightforward manner. Predicted spawning for the client predicted player object. The object is predicted so the input handling applies immediately. Therefore, it doesn't need to be delay spawned. While the snapshot data for this object arrives, the update system applies the data directly to the object and then plays back the local inputs which have happened since that time, and corrects mistakes in the prediction. Predicted spawning for player spawned objects. These are objects that the player input spawns, like in-game bullets or rockets that the player fires.
Implement Predicted Spawning for player spawned objectsThe spawn code needs to run on the client, in the client prediction system. The spawn should use the predicted client version of the ghost prefab and add a PredictedGhostSpawnRequestComponent to it. Then, when the first snapshot update for the entity arrives it will apply to that predict spawned object (no new entity is created). After this, the snapshot updates are applied the same as in the predicted spawning for client predicted player object model. To create the prefab for predicted spawning, you should use the utility method GhostCollectionSystem.CreatePredictedSpawnPrefab.You need to implement some specific code to handle the predicted spawning for player spawned objects. You need to create a system updating in the ClientSimulationSystemGroup after GhostSpawnClassificationSystem. The system needs to go through the GhostSpawnBuffer buffer stored on a singleton with a GhostSpawnQueueComponent. For each entry in that list it should compare to the entries in the PredictedGhostSpawn buffer on the singleton with a PredictedGhostSpawnList component. If the two entries are the same the classification system should set the PredictedSpawnEntity property in the GhostSpawnBuffer and remove the entry from GhostSpawnBuffer.NetCode spawns entities on clients when there is a Prefab available for it. Pre spawned ghosts will work without any special consideration since they are referenced in a sub scene, but for manually spawned entities you must make sure that the prefabs exist on the client. You make sure that happens by having a component in a scene which references the prefab you want to spawn.
We are now on the third type of spawning listed above, "Predicted spawning for player spawned objects". Asteroids were "Delayed or interpolated spawning".
A fair amount of the workflow in this section is similar to spawning the Player prefab from the previous section because it is also predicted. We will have a BulletGhostSpawnClassificationSystem that is similar to PlayerGhostSpawnClassificationSystem. However, there will be a bit of a difference in workflow within BulletGhostSpawnClassificationSystem when it comes to identifying the client's bullets.
- Navigate to the Bullet prefab
- Add GhostAuthoringComponent
- Name = Bullet
- Importance = 200
- Supported Ghost Mode = All
- Default Ghost Mode = Owner Predicted
- Optimization Mode = Dynamic
- Check "Has Owner"

Updating the Bullet prefab
- We are going to change our implementation of rate limiting from rate limiting on the client side, to rate limiting on the server side
- This is more line with the authoritative model, the server should be in control of these limits
- Paste the code snippet below into InputSystem.cs:
using UnityEngine;
using Unity.Entities;
using Unity.NetCode;
//This is a special SystemGroup introduced in NetCode 0.5
//This group only exists on the client and is meant to be used when commands are being created
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class InputSystem : SystemBase
{
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//We need this sytem group so we can grab its "ServerTick" for prediction when we respond to Commands
private ClientSimulationSystemGroup m_ClientSimulationSystemGroup;
//We use this for thin client command generation
private int m_FrameCount;
protected override void OnCreate()
{
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We set our ClientSimulationSystemGroup who will provide its ServerTick needed for the Commands
m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
//The client must have loaded the game to spawn a player so we wait for the
//NetworkStreamInGame component added during the load game flow
RequireSingletonForUpdate<NetworkStreamInGame>();
}
protected override void OnUpdate()
{
bool isThinClient = HasSingleton<ThinClientComponent>();
if (HasSingleton<CommandTargetComponent>() && GetSingleton<CommandTargetComponent>().targetEntity == Entity.Null)
{
if (isThinClient)
{
// No ghosts are spawned, so create a placeholder struct to store the commands in
var ent = EntityManager.CreateEntity();
EntityManager.AddBuffer<PlayerCommand>(ent);
SetSingleton(new CommandTargetComponent{targetEntity = ent});
}
}
//We now have all our inputs
byte right, left, thrust, reverseThrust, selfDestruct, shoot;
right = left = thrust = reverseThrust = selfDestruct = shoot = 0;
//for looking around with mouse
float mouseX = 0;
float mouseY = 0;
//We are adding this difference so we can use "Num Thin Client" in "Multiplayer Mode Tools"
//These are the instructions if we are NOT a thin client
if (!isThinClient)
{
if (Input.GetKey("d"))
{
right = 1;
}
if (Input.GetKey("a"))
{
left = 1;
}
if (Input.GetKey("w"))
{
thrust = 1;
}
if (Input.GetKey("s"))
{
reverseThrust = 1;
}
if (Input.GetKey("p"))
{
selfDestruct = 1;
}
if (Input.GetKey("space"))
{
shoot = 1;
}
if (Input.GetMouseButton(1))
{
mouseX = Input.GetAxis("Mouse X");
mouseY = Input.GetAxis("Mouse Y");
}
}
else
{
// Spawn and generate some random inputs
var state = (int) Time.ElapsedTime % 3;
if (state == 0)
{
left = 1;
}
else {
thrust = 1;
}
++m_FrameCount;
if (m_FrameCount % 100 == 0)
{
shoot = 1;
m_FrameCount = 0;
}
}
//We are sending the simulationsystemgroup tick so the server can playback our commands appropriately
var inputTargetTick = m_ClientSimulationSystemGroup.ServerTick;
//Must declare local variables before using them in the .ForEach()
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
// This is how we will grab the buffer of PlayerCommands from the player prefab
var inputFromEntity = GetBufferFromEntity<PlayerCommand>();
TryGetSingletonEntity<PlayerCommand>(out var targetEntity);
Job.WithCode(() => {
if (isThinClient && shoot != 0)
{
// Special handling for thin clients since we can't tell if the ship is spawned or not
// This means every time we shoot we also send an RPC, but the Server protects against creating more Players
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent());
}
if (targetEntity == Entity.Null)
{
if (shoot != 0)
{
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent());
}
}
else
{
var input = inputFromEntity[targetEntity];
input.AddCommandData(new PlayerCommand{Tick = inputTargetTick, left = left, right = right, thrust = thrust, reverseThrust = reverseThrust,
selfDestruct = selfDestruct, shoot = shoot,
mouseX = mouseX,
mouseY = mouseY});
}
}).Schedule();
//We need to add the jobs dependency to the command buffer
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}

Updating InputSystem so that shooting rate-limiting happens on the server
- We need a way to store firing data on the player entity so the server can know if the client's firing system has "cooled down"
- So we are going to repurpose our BulletSpawnOffsetComponent and rename it to PlayerStateAndOffsetComponent
- We could make a new component for rate limiting but this would mean we would need to put 9 different components into our .ForEach() for our InputResponseSpawnSystem 😱
- Paste the code snippet below into PlayerStateAndOffsetComponent.cs:
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
public struct PlayerStateAndOffsetComponent : IComponentData
{
public float3 Value;
[GhostField]
public int State;
public uint WeaponCooldown;
}

Updating BulletOffsetComponent to PlayerStateAndOffsetComponent (better to do file renaming in Unity or else you will get a .meta warning)
- We also included a "State" field in the component that can be updated when the user is thrusting or firing
- Although we won't be doing anything with "State" in this project, you are totally free to add something like change a client's color when it is thrusting or firing, if you want! To do this update the State value and create a client-only system that updates Player prefab meshes based on the State value
- Just a thought!
- Since we updated BulletOffsetComponent we will also need to update our SetBulletSpawnOffset which references it
- We will not rename the SetBulletSpawnOffset system and can keep the system as the same name, because even though we are adding the PlayerStateAndOffsetComponent to the entity the primary purpose of this component is to set the bullet offset
- So the name still works (kind of)
- Paste the code snippet below into SetBulletSpawnOffset.cs:
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
public class SetBulletSpawnOffset : UnityEngine.MonoBehaviour, IConvertGameObjectToEntity
{
public GameObject bulletSpawn;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var bulletOffset = default(PlayerStateAndOffsetComponent);
var offsetVector = bulletSpawn.transform.position;
bulletOffset.Value = new float3(offsetVector.x, offsetVector.y, offsetVector.z);
dstManager.AddComponentData(entity, bulletOffset);
}
}

Updating SetBulletSpawnOffset to use PlayerStateAndOffsetComponent
- Now let's re-add our SetBulletSpawnOffset system onto our Player prefab and drag the Bullet Spawn GameObject into the bulletSpawn field
- This really isn't necessary, but sometimes updating components that are on prefabs causes errors so better to be safe than sorry

Removing and re-adding SetBulletOffset on the Player prefab (to be safe)
- Now we need to create the system that will be responding to Commands that have to do with spawning
- Similar to InputResponseMovementSystem, which responds to inputs for movement, we need to create InputResponseSpawnSystem (which responds to inputs for spawning)
- Create InputResponseSpawnSystem in the Mixed/Systems folder
- Paste the code snippet below into InputResponseSpawnSystem.cs:
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.NetCode;
using Unity.Networking.Transport.Utilities;
using Unity.Collections;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Jobs;
using UnityEngine;