(8, Allocator.Temp);
diff --git a/Documentation~/network-connection.md b/Documentation~/network-connection.md
index d35fd84..454dc12 100644
--- a/Documentation~/network-connection.md
+++ b/Documentation~/network-connection.md
@@ -1,31 +1,99 @@
# Network connection
-## NetCode + Unity Transport
+## Netcode + Unity Transport
-The network connection uses the [Unity Transport package](https://docs.unity3d.com/Packages/com.unity.transport@latest) and stores each connection as an entity. Each connection entity has a [NetworkStreamConnection](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamConnection.html) component with the `Transport` handle for the connection. The connection also has a `NetworkStreamDisconnected` component for one frame, after it disconnects and before the entity is destroyed.
+The network connection uses the [Unity Transport package](https://docs.unity3d.com/Packages/com.unity.transport@latest) and stores each connection as an entity. Each connection entity has a [NetworkStreamConnection](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamConnection.html) component with the `Transport` handle for the connection. When the connection is closed, either because the server disconnected the user or the client request to disconnect, the the entity is destroyed.
To request disconnect, add a `NetworkStreamRequestDisconnect` component to the entity. Direct disconnection through the driver is not supported. Your game can mark a connection as being in-game, with the `NetworkStreamInGame` component. Your game must do this; it is never done automatically.
> [!NOTE]
-> Before the component is added to the connection, the client doesn’t send commands, nor does the server send snapshots.
+> Before the `NetworkStreamInGame` component is added to the connection, the client does not send commands, nor does the server send snapshots.
-To store commands in the correct buffer when not using auto command target, each connection has a `CommandTargetComponent` which must point to the entity where the received commands need to be stored. Your game is responsible for keeping this entity reference up to date.
+To target which entity should receive the player commands, when not using the `AutoCommandTarget` feature or for having a more manual control,
+each connection has a [CommandTargetComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.CommandTargetComponent.html)
+which must point to the entity where the received commands need to be stored. Your game is responsible for keeping this entity reference up to date.
-Each connection has three incoming buffers for each type of stream, command, RPC, and snapshot. There is also an outgoing buffer for RPCs. Snapshots and commands are gathered and sent in their respective send systems. When the client receives a snapshot it is available in the incoming snapshot buffer. The same method is used for the command stream and the RPC stream.
+### Ingoing buffers
+Each connection can have up to three incoming buffers, one for each type of stream: commands, RPCs and snapshot (client-only).
+[IncomingRpcDataStreamBufferComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.IncomingRpcDataStreamBufferComponent.html)
+[IncomingCommandDataStreamBufferComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.IncomingCommandDataStreamBufferComponent.html)
+[IncomingSnapshotDataStreamBufferComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.IncomingSnapshotDataStreamBufferComponent.html)
-When your game starts, it must tell the netcode to manually start listening for a connection on the server, or connect to a server from the client. This isn’t done automatically because a default has not been set. To establish a connection, you must get the `NetworkStreamReceiveSystem` from the client World for Connect, and the server World for Listen, and then call either `Connect` or `Listen` on it.
+When a client receive a snapshot from the server, the message is queued into the buffer and processed later by the [GhostReceiveSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.IncomingSnapshotDataStreamBufferComponent.html).
+Similarly, RPCs and Commands follow the sample principle. The messages are gathered first by the [NetworkStreamReceiveSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamReceiveSystem.html) and consumed then by
+the respective rpc and command receive system.
+> [!NOTE]
+> Server connection does not have an IncomingSnapshotDataStreamBufferComponent.
-## Network simulation
-Unity Transport provides a `SimulatorUtility`, which is available (and configurable) in the NetCode package. Access it via `Multiplayer > PlayMode Tools`. Non-zero values for delay, jitter or packet loss will enable network simulation.
+### Outgoing buffers
+Each connection can have up to two outgoing buffers: one for RPCs and one for commands (client only).
+[OutgoingRpcDataStreamBufferComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.OutgoingRpcDataStreamBufferComponent.html)
+[OutgoingCommandDataStreamBufferComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.OutgoingCommandDataStreamBufferComponent.html)
-> [!NOTE]
-> These simulator settings are applied on a per-packet basis (i.e. each way).
+When commands are produced, they are first queued into the outgoing buffer, that is flushed by client at regular interval (every new tick). Rpc messages follow the sample principle: they are gathered first by their respective send system,
+that encode them into the buffer first. Then, the [RpcSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.OutgoingCommandDataStreamBufferComponent.html) will flush the RPC in queue
+(by coalescing multiple messages in one MTU) at regular interval.
+
+## Connection Flow
+When your game starts, the Netcode for Entities package neither automatically connect the client to server, nor make the server start listening to a specific port. In particular the default `ClientServerBoostrap` just create the client and
+server worlds. It is up to developer to decide how and when the server and client open their communication channel.
+
+There are different way to do it:
+- Manually start listening for a connection on the server, or connect to a server from the client using the `NetworkStreamDriver`.
+- Automatically connect and listen by using the `AutoConnectPort` (and relative `DefaultConnectAddress`).
+- By creating a `NetworkStreamRequestConnect` and/or `NetworkStreamRequestListen` request in the client and/ot server world respectively.
+
+### Manually Listen/Connect
+To establish a connection, you must get the [NetworkStreamDriver](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamDriver.html) singleton (present on both client and server worlds)
+and then call either `Connect` or `Listen` on it.
+
+### Using the AutoConnectPort
+The `ClientServerBoostrap` contains two special properties that can be used to instruct the boostrap the server and client to automatically listen and connect respectively.
+- [AutoConnectPort](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerBootstrap.html#Unity_NetCode_ClientServerBootstrap_AutoConnectPort)
+- [DefaultConnectAddress](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerBootstrap.html#Unity_NetCode_ClientServerBootstrap_DefaultConnectAddress)
+- [DefaultListenAddress](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerBootstrap.html#Unity_NetCode_ClientServerBootstrap_DefaultListenAddress)
+
+In order to setup the `AutoConnectPort` you should create you custom [bootstrap](client-server-worlds.md#bootstrap) and setting a value different than 0 for the `AutoConnectPort`
+before creating your worlds. For example:
+
+```c#
+public class AutoConnectBootstrap : ClientServerBootstrap
+{
+ public override bool Initialize(string defaultWorldName)
+ {
+ // This will enable auto connect.
+ AutoConnectPort = 7979;
+ // Create the default client and server worlds, depending on build type in a player or the Multiplayer PlayMode Tools in the editor
+ CreateDefaultClientServerWorlds();
+ return true;
+ }
+}
+```
+The server will start listening at the wildcard address (`DefaultConnectAddress`:`AutoConnectPort`). The `DefaultConnectAddress` is by default set to `NetworkEndpoint.AnyIpv4`.
+The client will start connecting to server address (`DefaultConnectAddress`:`AutoConnectPort`). The `DefaultConnectAddress` is by default set to to `NetworkEndpoint.Loopback`.
> [!NOTE]
-> Enabling network simulation will force the Unity Transport's network interface to be a full UDP socket. Otherwise, we'll use IPC (Inter-Process Communication). See `DefaultDriverConstructor.cs`.
+> In the editor, the Playmode tool allow you to "override" both the AutoConnectAddress and AutoConnectPort. **The value is the playmode tool take precedence.**.
-We strongly recommend that you frequently test your gameplay with the simulator enabled, as it more closely resembles real-world conditions.
+### Controlling the connection flow using NetworkStreamRequest
+Instead of invoking and calling methods on the [NetworkStreamDriver](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamDriver.html) you can instead create:
+
+- A [NetworkStreamRequestConnect](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamRequestConnect.html) singleton to request a connection to the desired server address/port.
+- A [NetworkStreamRequestListen](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamRequestListen.html) singleton to make the server start listening at the desired address/port.
+
+```csharp
+//On the client world, create a new entity with a NetworkStreamRequestConnect. It will be consumed by NetworkStreamReceiveSystem later.
+var connectRequest = clientWorld.EntityManager.CreatEntity(typeof(NetworkStreamRequestConnect));
+EntityManager.SetComponentData(connectRequest, new NetworkStreamRequestConnect { Endpoint = serverEndPoint });
+
+//On the server world, create a new entity with a NetworkStreamRequestConnect. It will be consumed by NetworkStreamReceiveSystem later.
+var listenRequest = serverWorld.EntityManager.CreatEntity(typeof(NetworkStreamRequestListen));
+EntityManager.SetComponentData(connectRequest, new NetworkStreamRequestListen { Endpoint = serverEndPoint });
-Network simulation can be enabled (**in development builds only! DotsRuntime is also not supported!**) via the command line argument `--loadNetworkSimulatorJsonFile [optionalJsonFilePath]`.
-Alternatively, `--createNetworkSimulatorJsonFile [optionalJsonFilePath]` can be passed if you want the file to be auto-generated (in the case that it's not found).
-It expects a json file containing `SimulatorUtility.Parameters`.
-Passing in either parameter will **always** enable a simulator profile, as we fallback to using the `DefaultSimulatorProfile` if the file is not found (or generated).
+```
+
+The request will be then consumed at runtime by the [NetworkStreamReceiveSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkStreamReceiveSystem.html).
+
+## Network Simulator
+Unity Transport provides a [SimulatorUtility](playmode-tool.md#networksimulator), which is available (and configurable) in the Netcode package. Access it via `Multiplayer > PlayMode Tools`.
+
+We strongly recommend that you frequently test your gameplay with the simulator enabled, as it more closely resembles real-world conditions.
diff --git a/Documentation~/networked-cube.md b/Documentation~/networked-cube.md
new file mode 100644
index 0000000..84b9a7d
--- /dev/null
+++ b/Documentation~/networked-cube.md
@@ -0,0 +1,492 @@
+# Networked Cube
+
+Make sure you have set up the project correctly using the [installation guide](installation.md) before starting your adventure of creating a simple client-server based simulation.
+
+This tutorial briefly introduces the most common concepts involved in making a client-server based game.
+
+## Creating an initial Scene
+
+To begin, set up a way to share data between the client and the server. We achieve this separation in Netcode for Entities by creating [a different World](client-server-worlds.md) for the server and each client. To share data between the server and the client:
+
+1. Right-click within the Hierarchy window in the Unity Editor.
+2. Select __New Subscene > Empty Scene__...
+3. Name the new scene "SharedData".
+
+![](images/create_subscene.png)
+
+
+
+Once this is set up , we want to spawn a plane in both the client and the server world. To do this, right click the __SharedData__ Sub Scene and select __3D Object > Plane__ which then creates a planes nested under __SharedData__.
+
+![Scene with a plane](images/initial-scene.png)
_Scene with a plane_
+
+If you select Play, then select __Window > Entities > Hierarchy__, you can see two worlds (ClientWorld and ServerWorld), each with the SharedData Scene with the Plane that you just created.
+
+![Hierarcy View](images/hierarchy-view.png)
_Hierarchy View_
+
+## Establish a connection
+
+To enable communication between the client and server, you need to establish a [connection](network-connection.md). In Netcode for Entities, the simplest way of achieving this is to use the auto-connect feature. You can use the auto-connect feature by inheriting from the `ClientServerBootstrap`, then setting the `AutoConnectPort` to your chosen port.
+
+Create a file called *Game.cs* in your __Assets__ folder and add the following code to the file:
+
+```c#
+using System;
+using Unity.Entities;
+using Unity.NetCode;
+
+// Create a custom bootstrap, which enables auto-connect.
+// The bootstrap can also be used to configure other settings as well as to
+// manually decide which worlds (client and server) to create based on user input
+[UnityEngine.Scripting.Preserve]
+public class GameBootstrap : ClientServerBootstrap
+{
+ public override bool Initialize(string defaultWorldName)
+ {
+ AutoConnectPort = 7979; // Enabled auto connect
+ return base.Initialize(defaultWorldName); // Use the regular bootstrap
+ }
+}
+```
+
+## Communicate with the server
+
+When you are connected, you can start communication. A critical concept in Netcode for Entities is the concept of `InGame`. When a connection is marked with `InGame` it tells the simulation its ready to start [synchronizing](synchronization.md).
+
+You communicate with Netcode for Entities by using `RPC`s. So to continue create a RPC that acts as a "Go In Game" message, (for example, tell the server that you are ready to start receiving [snapshots](ghost-snapshots.md)).
+
+Create a file called *GoInGame.cs* in your __Assets__ folder and add the following code to the file.
+
+```c#
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+using UnityEngine;
+
+// RPC request from client to server for game to go "in game" and send snapshots / inputs
+public struct GoInGameRequest : IRpcCommand
+{
+}
+
+// When client has a connection with network id, go in game and tell server to also go in game
+[BurstCompile]
+[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
+public partial struct GoInGameClientSystem : ISystem
+{
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll()
+ .WithNone();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ }
+
+ [BurstCompile]
+ public void OnDestroy(ref SystemState state)
+ {
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
+ foreach (var (id, entity) in SystemAPI.Query>().WithEntityAccess().WithNone())
+ {
+ commandBuffer.AddComponent(entity);
+ var req = commandBuffer.CreateEntity();
+ commandBuffer.AddComponent(req);
+ commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent { TargetConnection = entity });
+ }
+ commandBuffer.Playback(state.EntityManager);
+ }
+}
+
+// When server receives go in game request, go in game and delete request
+[BurstCompile]
+[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+public partial struct GoInGameServerSystem : ISystem
+{
+ private ComponentLookup networkIdFromEntity;
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll()
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ networkIdFromEntity = state.GetComponentLookup(true);
+ }
+
+ [BurstCompile]
+ public void OnDestroy(ref SystemState state)
+ {
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var worldName = state.WorldUnmanaged.Name;
+
+ var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
+ networkIdFromEntity.Update(ref state);
+
+ foreach (var (reqSrc, reqEntity) in SystemAPI.Query>().WithAll().WithEntityAccess())
+ {
+ commandBuffer.AddComponent(reqSrc.ValueRO.SourceConnection);
+ var networkIdComponent = networkIdFromEntity[reqSrc.ValueRO.SourceConnection];
+
+ Debug.Log($"'{worldName}' setting connection '{networkIdComponent.Value}' to in game");
+
+ commandBuffer.DestroyEntity(reqEntity);
+ }
+ commandBuffer.Playback(state.EntityManager);
+ }
+
+}
+
+```
+
+## Create a ghost Prefab
+
+To synchronize something across a client/server setup, you need to create a definition of the networked object, called a **ghost**.
+
+To create a ghost Prefab:
+
+1. Create a cube in the Scene by right-clicking on the Scene, then selecting __3D Object > Cube__).
+2. Select the __Cube GameObject__ under the __Scene__ and drag it into the Project’s __Asset__ folder. This creates a Prefab of the Cube.
+3. After creating the Prefab, you can delete the cube from the scene, but __do not__ delete the Prefab.
+
+![Create a Cube Prefab](images/cube-prefab.png)
_Create a Cube Prefab_
+
+To identify and synchronize the Cube Prefab inside Netcode for Entities, you need to create a `IComponent` and Author it. To do so create a new file called *CubeComponentAuthoring.cs* and we enter the following:
+
+```c#
+using Unity.Entities;
+using UnityEngine;
+
+public struct CubeComponent : IComponentData
+{
+}
+
+[DisallowMultipleComponent]
+public class CubeComponentAuthoring : MonoBehaviour
+{
+ class MovableCubeComponentBaker : Baker
+ {
+ public override void Bake(CubeComponentAuthoring authoring)
+ {
+ CubeComponent component = default(CubeComponent);
+ AddComponent(component);
+ }
+ }
+}
+```
+
+If you want to add a serialized value to the component, you can use the __GhostField Attribute__:
+
+```c#
+using Unity.Entities;
+using Unity.NetCode;
+
+[GenerateAuthoringComponent]
+public struct CubeComponent : IComponentData
+{
+ [GhostField]
+ public int ExampleValue;
+}
+
+[DisallowMultipleComponent]
+public class CubeComponentAuthoring : MonoBehaviour
+{
+ class MovableCubeComponentBaker : Baker
+ {
+ public override void Bake(CubeComponentAuthoring authoring)
+ {
+ CubeComponent component = default(CubeComponent);
+ AddComponent(component);
+ }
+ }
+}
+```
+
+Once you create this component, add it to the Cube Prefab. Then, in the Inspector, add the __Ghost Authoring Component__ to the Prefab.
+
+When you do this, Unity will automatically serialize the Translation and Rotation components.
+
+Before you can move the cube around, you must change some settings in the newly added __Ghost Authoring Component__:
+
+1. Check the __Has Owner__ box. This automatically adds and checks a new property called _Support Auto Command Target_ (more on this later).
+2. Change the __Default Ghost Mode to Owner Predicted__. You need to set the __NetworkId__ member of the __Ghost Owner Component__ in your code (more on this later). This makes sure that you predict your own movement.
+
+![The Ghost Authoring component](images/ghost-config.png)
_The Ghost Authoring component_
+
+
+## Create a spawner
+To tell Netcode for Entities which Ghosts to use, you need to reference the prefabs from the sub-scene. First, create a new component for the spawner: create a file called _CubeSpawnerAuthoring.cs_ and add the following code:
+
+```c#
+using Unity.Entities;
+using UnityEngine;
+
+public struct CubeSpawner : IComponentData
+{
+ public Entity Cube;
+}
+
+[DisallowMultipleComponent]
+public class CubeSpawnerAuthoring : MonoBehaviour
+{
+ public GameObject Cube;
+
+ class NetCubeSpawnerBaker : Baker
+ {
+ public override void Bake(CubeSpawnerAuthoring authoring)
+ {
+ CubeSpawner component = default(CubeSpawner);
+ component.Cube = GetEntity(authoring.Cube);
+ AddComponent(component);
+ }
+ }
+}
+```
+
+1. Right-click on SharedData and select __Create Empty__.
+2. Rename it to __Spawner__, then add a __CubeSpawner__.
+3. Because both the client and the server need to know about these Ghosts, add it to the __SharedData Sub Scene__.
+4. In the Inspector, drag the Cube prefab to the Cube field of the spawner.
+
+![Ghost Spawner settings](images/ghost-spawner.png)
_Ghost Spawner settings_
+
+### Spawning our prefab
+
+To spawn the prefab, you need to update the _GoInGame.cs_ file. If you recall from earlier, you must send a __GoInGame__ `RPC` when you are ready to tell the server to start synchronizing. You can update that code to actually spawn our cube as well.
+
+```diff
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+
+public struct GoInGameRequest : IRpcCommand
+{
+}
+
+[BurstCompile]
+[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
+public partial struct GoInGameClientSystem : ISystem
+{
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
++ state.RequireForUpdate();
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll()
+ .WithNone();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ }
+
+ [BurstCompile]
+ public void OnDestroy(ref SystemState state)
+ {
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
+ foreach (var (id, entity) in SystemAPI.Query>().WithEntityAccess().WithNone())
+ {
+ commandBuffer.AddComponent(entity);
+ var req = commandBuffer.CreateEntity();
+ commandBuffer.AddComponent(req);
+ commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent { TargetConnection = entity });
+ }
+ commandBuffer.Playback(state.EntityManager);
+ }
+}
+
+[BurstCompile]
+// When server receives go in game request, go in game and delete request
+[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+public partial struct GoInGameServerSystem : ISystem
+{
+ private ComponentLookup networkIdFromEntity;
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
++ state.RequireForUpdate();
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll()
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ networkIdFromEntity = state.GetComponentLookup(true);
+ }
+
+ [BurstCompile]
+ public void OnDestroy(ref SystemState state)
+ {
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
++ var prefab = SystemAPI.GetSingleton().Cube;
++ state.EntityManager.GetName(prefab, out var prefabName);
+ var worldName = new FixedString32Bytes(state.WorldUnmanaged.Name);
+
+ var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
+ networkIdFromEntity.Update(ref state);
+
+ foreach (var (reqSrc, reqEntity) in SystemAPI.Query>().WithAll().WithEntityAccess())
+ {
+ commandBuffer.AddComponent(reqSrc.ValueRO.SourceConnection);
+ var networkIdComponent = networkIdFromEntity[reqSrc.ValueRO.SourceConnection];
+
+- UnityEngine.Debug.Log($"'{worldName}' setting connection '{networkIdComponent.Value}' to in game");
++ UnityEngine.Debug.Log($"'{worldName}' setting connection '{networkIdComponent.Value}' to in game, spawning a Ghost '{prefabName}' for them!");
+
++ var player = commandBuffer.Instantiate(prefab);
++ commandBuffer.SetComponent(player, new GhostOwnerComponent { NetworkId = networkIdComponent.Value});
+
++ // Add the player to the linked entity group so it is destroyed automatically on disconnect
++ commandBuffer.AppendToBuffer(reqSrc.ValueRO.SourceConnection, new LinkedEntityGroup{Value = player});
+ commandBuffer.DestroyEntity(reqEntity);
+ }
+ commandBuffer.Playback(state.EntityManager);
+ }
+}
+```
+
+If you press Play now, you should see the replicated cube in the game view and the Entity Hierarchy view.
+
+![Replicated Cube](images/replicated-cube.png)
_Replicated Cube_
+
+## Moving the Cube
+
+Because you used the _Support Auto Command Target_ feature when you set up the ghost component, you can take advantage of the `IInputComponentData` struct for storing input data. This struct dictates what you will be serializing and deserializing as the input data. You also need to create a System that will fill in our input data.
+
+Create a script called *CubeInputAuthoring.cs* and add the following code:
+
+```c#
+using Unity.Entities;
+using Unity.NetCode;
+using UnityEngine;
+
+[GhostComponent(PrefabType=GhostPrefabType.AllPredicted)]
+public struct CubeInput : IInputComponentData
+{
+ public int Horizontal;
+ public int Vertical;
+}
+
+[DisallowMultipleComponent]
+public class CubeInputAuthoring : MonoBehaviour
+{
+ class CubeInputBaking : Unity.Entities.Baker
+ {
+ public override void Bake(CubeInputAuthoring authoring)
+ {
+ AddComponent();
+ }
+ }
+}
+
+[UpdateInGroup(typeof(GhostInputSystemGroup))]
+public partial struct SampleCubeInput : ISystem
+{
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ }
+
+ public void OnDestroy(ref SystemState state)
+ {
+ }
+
+ public void OnUpdate(ref SystemState state)
+ {
+ bool left = UnityEngine.Input.GetKey("left");
+ bool right = UnityEngine.Input.GetKey("right");
+ bool down = UnityEngine.Input.GetKey("down");
+ bool up = UnityEngine.Input.GetKey("up");
+
+ foreach (var playerInput in SystemAPI.Query>().WithAll())
+ {
+ playerInput.ValueRW = default;
+ if (left)
+ playerInput.ValueRW.Horizontal -= 1;
+ if (right)
+ playerInput.ValueRW.Horizontal += 1;
+ if (down)
+ playerInput.ValueRW.Vertical -= 1;
+ if (up)
+ playerInput.ValueRW.Vertical += 1;
+ }
+ }
+}
+```
+
+Add the `CubeInputAuthoring` component to your Cube Prefab, and then finally, create a system that can read the `CubeInput` and move the player.
+
+Create a new file script called `CubeMovementSystem.cs` and add the following code:
+
+```c#
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.NetCode;
+using Unity.Transforms;
+using Unity.Collections;
+using Unity.Burst;
+
+[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
+[BurstCompile]
+public partial struct CubeMovementSystem : ISystem
+{
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll()
+ .WithAll()
+ .WithAllRW();
+ var query = state.GetEntityQuery(builder);
+ state.RequireForUpdate(query);
+ }
+
+ [BurstCompile]
+ public void OnDestroy(ref SystemState state)
+ {
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var moveJob = new MoveCubeJob
+ {
+ fixedCubeSpeed = SystemAPI.Time.DeltaTime * 4
+ };
+ state.Dependency = moveJob.ScheduleParallel(state.Dependency);
+ }
+
+ [BurstCompile]
+ [WithAll(typeof(Simulate))]
+ partial struct MoveCubeJob : IJobEntity
+ {
+ public float fixedCubeSpeed;
+ public void Execute(CubeInput playerInput, ref Translation trans)
+ {
+ var moveInput = new float2(playerInput.Horizontal, playerInput.Vertical);
+ moveInput = math.normalizesafe(moveInput) * fixedCubeSpeed;
+ trans.Value += new float3(moveInput.x, 0, moveInput.y);
+ }
+ }
+}
+```
+
+## Test the code
+
+Now you have set up your code, open __Multiplayer > PlayMode Tools__ and set the __PlayMode Type__ to __Client & Server__. Enter Play Mode, and the Cube spawns. Press the __Arrow__ keys to move the Cube around.
diff --git a/Documentation~/physics.md b/Documentation~/physics.md
index c5c8bdf..e86c13f 100644
--- a/Documentation~/physics.md
+++ b/Documentation~/physics.md
@@ -1,74 +1,78 @@
# Physics
-The NetCode package has some integration with Unity Physics which makes it easier to use physics in a networked game. The integration handles interpolated ghosts with physics, and you can manually enable support for predicted ghosts with physics.
+
+The Netcode package has some integration with Unity Physics which makes it easier to use physics in a networked game. The integration handles interpolated ghosts with physics, and support for predicted ghosts with physics.
+
+This works without any configuration but will assume all dynamic physics objects are ghosts, so either fully simulated by the server (interpolated ghosts), or by both with the client also simulating forward (at predicted/server tick) and server correcting prediction errors (predicted ghosts), the two types can be mixed together. To run the physics simulation only locally for certain objects some setup is required.
## Interpolated ghosts
-For interpolated ghosts it is important that the physics simulation only runs on the server.
+
+For interpolated ghosts it is important that the physics simulation only runs on the server.
On the client the ghosts position and rotation are controlled by the snapshots from the server and the client should not run the physics simulation for interpolated ghosts.
-In order to make sure this is true NetCode will add a [`PhysicsMassOverride`](https://docs.unity3d.com/Packages/com.unity.physics@0.6/api/Unity.Physics.PhysicsMassOverride.html) component to every ghost which is also a dynamic physics object on the client.
-The `PhysicsMassOverride` will mark the objects as kinematic - meaning they will not be moved by the physics simulation.
-Furthermore, for interpolated ghost by default we setup the PhysicsMassOverride such that the the PhysicVelocity component velocities are ignored
-(the physis motion will have zero linear and angular velocity) but preserved (so they will never reset forcibly to zero).
-This means that when using NetCode and Unity Physics you cannot use `PhysicsMassOverride` for game specific purposes on the client.
-## Predicted Phyics Simulation
-It is possible to use physics simulation for predicted ghosts. We will use the term _Predicted Physics_ to indidicate that the physics simulation run in the prediction loop.
-In order to to use physics simulation for predicted ghosts you must enable it by creating a singleton with the [`PredictedPhysicsConfig`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedPhysicsConfig.html) component.
-The singleton must exist on both the client and server with compatible values (same simulation frequency).
-The component lets you specify the physics tick rate as a multiple of the NetCode simulation tick rate, so you can run for example a 60Hz game simulation with 120Hz physics simulation.
+In order to make sure this is true Netcode will disable the `Simulate` component data tag on clients on appropriate ghost entities at the beginning on each frame. That make the physics object `kinematic` and they will not be moved by the physics simulation.
-When the singleton exists, two distinct physics simulation islands, normally referred as _Physics Worlds_, will be present:
-- A **Predicted Physics World**: will contains all the physics entities (interpolated and predicted ghosts, environment, etc...) for which the simulation need to run on both the client and server.
-- A **Client-Only Physics World**: only simulated on the client, can be used to run VFX, particles and any other sort of physics interaction that does not need to be replicated.
+In particular:
-![Multiple Physics World](images/multiphysicsworld.jpg)
+- The `PhysicsVelocity` will be ignored (set to zero).
+- Yhe `Translation` and `Rotation` are preserved.
-The PredictedPhysics simulation run inside the [`PredictedPhysicsSystemGroup`]((https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedPhysicsSystemGroup.html)) as part of the prediction loop.
-The PredictedPhysicsSystemGroup uses for the simulation a specific implementation of the physics systems and provide the same flow and phase you are already familiar with (the build, step, export etc).
-The Client-Only physics simulation run instead in the FixedUpdateSimulationGroup and uses the built-in physics systems.
+## Predicted ghosts and physics simulation
+By the term _Predicted Physics_ we mean that the physics simulation runs in the prediction loop (possibly multiple times per update from the tick of the last received snapshot update) on the client, as well as running normally on the server.
-The two simulations can use different fixed-time steps and are not supposed to be in sync, meaning that in the same frame it is possible to have both, or only one of them to be executed independently.
-Furthermore on the client, because of the client-prediction. **when a rollback occurs the simulation may runs multiple times in the same frame, one the for each rollback tick**.
+During initialization Netcode will move the `PhysicsSystemGroup` and all `FixedStepSimulationSystemGroup` systems into the `PredictedFixedStepSimulationSystemGroup`. This group is the predicted version of `FixedStepSimulationSystemGroup`, so everything here will be called multiple times up to the required number of predicted ticks. These groups are then only updated when there is actually a dynamic predicted physics ghost present in the world.
-NetCode rely on the multi-physics-world feature of the Unity.Physics package to implement this logic. As part of the conversion process, a _PhysicWorldIndex_ shared component is added to all the physics entities, indicating
-in which world the entity should be part of. **It is responsibility of the user to setup properly their prefab using the PhysicBody inspector, to make them run in the correct physics world**.
+All predicted ghosts with physics components will run this kind of simulation when they are dynamic. Like with interpolated ghosts the `Simulate` tag will be enabled/disabled as appropriate at the beginning of each predicted frame, but this time multiple simulation steps might be needed.
-The default configuration for of the predicted and client-only physics world indices is the following:
-- PredictedPhysicsWorld: index 0
-- ClientOnlyPhysicsWorld: index 1
+Since the physics simulation can be quite CPU intensive it can spiral out of control when it needs to run multiple times. Needing to predict multiple simulation frames could then result in needing to run multiple ticks in one frame as the fixed timesteps falls behind the simulation tick rate, making the situation worse. On server it may be beneficial to enable simulation batching in the [`ClientServerTickRate`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerTickRate.html) component, see the `MaxSimulationStepBatchSize` and `MaxSimulationStepsPerFrame` options. On clients there are options for prediction batching exposed in the [`ClientTickRate`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientTickRate.html), see `MaxPredictionStepBatchSizeFirstTimeTick` and `MaxPredictionStepBatchSizeRepeatedTick`.
-It is possible to override the default settings by adding a [`PredictedPhysicsWorldIndexOverride`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PhysicsWorldIndicesOverride.html) component
-to the entity that hold the PredictedPhysicsConfig component.
+### Using lag compensation predicted collision worlds
+When using predicted physics the client will see his predicted physics objects at a slightly different view as the _correct_ authoritative view seen by the server, since it is forward predicting where objects will be at the current server tick. When interacting with such physics objects there is a lag compensation system available so the server can _look up_ what collision world the client saw at a particular tick (to for example better account for if he hit a particular collider). This is enabled via the `EnableLagCompensation` tick in the [`NetCodePhysicsConfig`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetCodePhysicsConfig.html) component. Then you can use the [`PhysicsWorldHistorySingleton`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PhysicsWorldHistorySingleton.html) to query for the collision world at a particular tick.
-### Interaction in between predicted and client-only physics entities
-There are situation when you would like to make the ghosts interact with physics object that are present only on the client (ex: debris). However, but being them part of different simulation islands they can't interact each-other.
-NetCode provides for that usecase a specific workflow using `Ghost Physics Proxy` entities.
+## Multiple physics worlds
-For each entity in physics entity present in the predicted world you would like to interact with the client-only world, it is necessary to spawn/create a _proxy/companion_ entity, configured to run in the client-only physics simulation.
-The simulated ghost entity in the predicted world will then be used to _drive_ the proxy by copying the necessary component data and setup the physics velocity to let the proxy move and interact with the other physics entities in the client-only world.
+Predicted simulation will work by default and all physics objects in the world should be ghosts. To enable client-only physics simulation (for example to use it run VFX, particles and any other sort of physics interaction that does not need to be replicated) another physics world needs to be created for it.
-![Ghost Proxy](images/physicsproxy.png)
+By default, the main physics world at index 0 will be the _predicted physics world_, but a separate _client-only physics world_ can also be created, running it's own distinct simulation. This can be done by implementing a custom physics system group and providing it with a new physics world index. Creating a client only physics world at index 1 can be done most simply like this:
-The proxy entity must have PhysicsBody, PhysicsMass, PhysicsMassOverride a PhysicsVelocity components and can optionally have any other arbitrary number of components types.
+```c#
+[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
+public partial class VisualizationPhysicsSystemGroup : CustomPhysicsSystemGroup
+{
+ public VisualizationPhysicsSystemGroup() : base(1, true)
+ {}
+}
+```
-For ghosts entities, the [`GenerateGhostPhysicsProxy`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GenerateGhostPhysicsProxy.html) component can be added to the ghost prefab
-to let make NetCode system automatically create a physics proxy when a ghost is spawned.
-It is possible to specify your own prefab to use or let NetCode auto-generated a default kinematic proxy entity with the minimal set of mandatory components.
+Where the arguments to the custom class constructor are the world index and boolean indicating if it should share static colliders with the main physics world. Physics simulation will here run in the usual `FixedUpdateSimulationGroup` as usual. See more about the `CustomPhysicsSystemGroup` in the [Unity Physics documentation](https://docs.unity3d.com/Packages/com.unity.physics@latest/index.html?subfolder=/manual/).
-For ghost configured to spawn proxy entity, a link in between the source predicted entity and the proxy is created. A `GhostPhysicsProxyReference` and a `PhysicsProxyGhostDriver` components, which provide an entity reference to the proxy and the driving/originating ghost, are added respectively to the ghost and the proxy entities.
+The two simulations can use different fixed-time steps and are not required to be in sync, meaning that in the same frame it is possible to have both, or only one of them to be executed independently.
+However, as mentioned in the previous section, for the predicted simulation **when a rollback occurs the simulation may runs multiple times in the same frame, one the for each rollback tick**. The client only simulation of course just runs once as normally.
-![Proxy Link](images/proxylink.png)
+When a client only physics world exists, all non-ghost dynamic physics objects can be moved to that. This can be configured in the [`NetCodePhysicsConfig`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetCodePhysicsConfig.html) component which can be added to a subscene. By setting the `ClientNonGhostWorldIndex` there to the client only physics world index all dynamic non-ghosts will be moved there.
-The ghost proxy position and rotation and are automatically handled by [`SyncGhostPhysicsProxies`](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.SyncGhostPhysicsProxies.html) system.
-By default the kinematic physics entity is moved using kinematic veloctiy, by altering the PhysicsVelocity component. It is possible to change the default behavior for the prefab by setting the
-`GenerateGhostPhysicsProxy.DriveMode` component property.
-Furthermore, it is possible to change that beahviour dynamically at runtime by setting the `PhysicsProxyGhostDriver.driveMode` property to the desired mode.
+As part of the entity baking process, a _PhysicsWorldIndex_ shared component is added to all the physics entities, indicating
+in which world the entity should be part of.
+> [!NOTE]
+> It is responsibility of the user to setup properly their prefab using the `PhysicBody` inspector, to make them run in the correct physics world.
+
+### Interaction in between predicted and client-only physics entities
+
+There are situation when you would like to make the ghosts interact with physics object that are present only on the client (ex: debris). However, them being a part of a different simulation islands they can't interact with each-other.
+The Physics package provides for that use-case a specific workflow using `Custom Physics Proxy` entities.
+
+For each physics entity present in the predicted world where you would like to interact with the client-only world, you need to add the `CustomPhysicsProxyAuthoring` component. The baking process will then automatically create a proxy entity with the necessary physics components (PhysicsBody, PhysicsMass, PhysicsVelocity) along with a [`CustomPhysicsProxyDriver`](https://docs.unity3d.com/Packages/com.unity.physics@latest/index.html?subfolder=/api/Unity.Physics.CustomPhysicsProxyDriver.html) which is the link to the root ghost entity. It will make a copy of the ghosts collider as well and configure the proxy physics body as kinematic. The simulated ghost entity in the predicted world will then be used to _drive_ the proxy by copying the necessary component data and setup the physics velocity to let the proxy move and interact with the other physics entities in the client-only world.
-**It is user responsibility to implement the systems that syncronize/copy any user-defined components when a custom prefab is provided.**
+The ghost proxy position and rotation and are automatically handled by [`SyncCustomPhysicsProxySystem`](https://docs.unity3d.com/Packages/com.unity.physics@latest/index.html?subfolder=/api/Unity.Physics.Systems.SyncCustomPhysicsProxySystem.html) system.
+By default the kinematic physics entity is moved using kinematic velocity, by altering the PhysicsVelocity component. It is possible to change the default behavior for the prefab by setting the
+`GenerateGhostPhysicsProxy.DriveMode` component property.
+Furthermore, it is possible to change that beahviour dynamically at runtime by setting the `PhysicsProxyGhostDriver.driveMode` property to the desired mode.
## Limitations
-As mentioned on this page there are some limitations you must be aware of to use physics and NetCode together.
-* NetCode will use `PhysicsMassOverride` on the client, you cannot use it for game specific purposes.
-* Physics simulation will not use partial ticks on the client, you must use physics interpolation if you want physics to update more frequently than it is simulating.
-* The Unity.Physics debug systems that does not work correctly in presence of multiple world (only the default physics world is displayed).
+
+As mentioned on this page there are some limitations you must be aware of to use physics and netcode together.
+
+- Physics simulation will not use partial ticks on the client, you must use physics interpolation if you want physics to update more frequently than it is simulating.
+- The Unity.Physics debug systems does not work correctly in presence of multiple world (only the default physics world is displayed).
diff --git a/Documentation~/playmode-tool.md b/Documentation~/playmode-tool.md
new file mode 100644
index 0000000..cf8aec8
--- /dev/null
+++ b/Documentation~/playmode-tool.md
@@ -0,0 +1,51 @@
+# PLAY MODE TOOL WINDOW
+The __PlayMode Tools__ window in the Unity Editor provide a set of utility to:
+- select what type of mode (client, server, client/server) you would like the game start.
+- enable and configure the [network simulator](network-connection.md#network-simulator).
+- configure the number of [thin-clients](client-server-worlds.md#thin-clients) to use.
+- changing the current logging level and enabling packet dumps.
+- connect/disconnect clients when in play-mode
+- showing bounding box gizmos.
+
+
+
+You can access __PlayMode Tools__, go to menu: __Multiplayer > PlayMode Tools__.
+
+| **Property** | **Description** |
+|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| __PlayMode Type__ | Choose to make Play Mode either __Client__ only, __Server__ only, or __Client & Server__. |
+| __Num Thin Clients__ | Set the number of thin clients. Thin clients cannot be presented, and never spawn any entities it receives from the server. However, they can generate fake input to send to the server to simulate a realistic load. |
+| __Simulate_Dedicated_Server__ | When enabled, in the editor the sub-scene for the server world are baked using the server settings. |
+
+When you enter Play Mode, from this window you can also connect and disconnect clients.
+When you change a client that Unity is presenting, it stops calling the update on the `ClientPresentationSystemGroup` for the Worlds which it should no longer present. As such, your code needs to be able to handle this situation, or your presentation code won’t run and all rendering objects you’ve created still exist.
+
+## NetworkSimulator
+The Network Simulator can be used to simulate some network condition while running your game in the editor.
+Once the simulator is enabled, you can set the packet delay, drop either manually (by setting your own value) or by selecting some provided `presets` (i.e 4G, Broadband, etc.).
+
+You can also specify your own settings, by setting custom values in the `RTT Delay`, `RTT Jitter` `PacketDrop` fields (or `Packet Delay`, `Packet Jitter` for `Packet View`).
+
+| **Property** | **Description** |
+|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| __RTTDelay__ | Use this property to emulate round trip time. The property delay the incoming and outgoing packet (in ms) such that the sum of the delays equals the specified value. |
+| __RTTJitter__ | Use this property to add a random value to the delay, which makes the delay a value between the delay you have set plus or minus the jitter value. For example, if you set __RTTDelay__ to 45 and __RTTJitter__ to 5, you will get a random value between 40 and 50. |
+| __PacketDrop__ | Use this property to simulate bad connections where not all packets arrive. Specify a value (as a percentage) and Netcode discards that percentage of packets from the total it receives. For example, set the value to 5 and Netcode discards 5% of all incoming and outgoing packets. |
+| __AutoConnectAddress (Client only)__ | Specify which server a client should connect to. This field only appears if you set __PlayMode Type__ to __Client__. The user code needs to read this value and connect because the connection flows are in user code. |
+| __AutoConnectPort (Client only)__ | Override and/or specify which port to use for both listening (server) and connecting (client)|
+
+
+> [!NOTE]
+> These simulator settings are applied on a per-packet basis (i.e. each way).
+> [!NOTE]
+> Enabling network simulation will force the Unity Transport's network interface to be a full UDP socket. Otherwise, when a both Client and Server worlds are present in the same process an IPC (Inter-Process Communication) connection is used instead.
+> See [DefaultDriverConstructor](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.IPCAndSocketDriverConstructor.html)
+
+We strongly recommend that you frequently test your gameplay with the simulator enabled, as it more closely resembles real-world conditions.
+
+### Initialize simulator from the command line
+Network simulation can be enabled (**in development builds only! DotsRuntime is also not supported!**) via the command line argument `--loadNetworkSimulatorJsonFile [optionalJsonFilePath]`.
+Alternatively, `--createNetworkSimulatorJsonFile [optionalJsonFilePath]` can be passed if you want the file to be auto-generated (in the case that it's not found).
+It expects a json file containing `SimulatorUtility.Parameters`.
+
+Passing in either parameter will **always** enable a simulator profile, as we fallback to using the `DefaultSimulatorProfile` if the file is not found (or generated).
diff --git a/Documentation~/prediction.md b/Documentation~/prediction.md
index 30271c1..01fa614 100644
--- a/Documentation~/prediction.md
+++ b/Documentation~/prediction.md
@@ -2,25 +2,70 @@
Prediction in a multiplayer games means that the client is running the same simulation as the server for the local player. The purpose of running the simulation on the client is so it can predictively apply inputs to the local player right away to reduce the input latency.
-Prediction should only run for entities which have the [PredictedGhostComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedGhostComponent.html). Unity adds this component to all predicted ghosts on the client and to all ghosts on the server. On the client, the component also contains some data it needs for the prediction - such as which snapshot has been applied to the ghost.
+Prediction should only run for entities which have the [PredictedGhostComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedGhostComponent.html).
+Unity adds this component to all predicted ghosts on the client and to all ghosts on the server. On the client, the component also contains some data it needs for the prediction - such as which snapshot has been applied to the ghost.
-The prediction is based on a [PredictedSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@0latest/index.html?subfolder=/api/Unity.NetCode.PredictedSimulationSystemGroup.html) which always runs at a fixed timestep to get the same results on the client and server.
+The prediction is based on a fixed timestep loop, controlled by the [PredictedSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@0latest/index.html?subfolder=/api/Unity.NetCode.PredictedSimulationSystemGroup.html),
+which runs on both client and server, and that usually contains the core part of the deterministic ghosts simulation.
## Client
The basic flow on the client is:
-* NetCode applies the latest snapshot it received from the server to all predicted entities.
-* While applying the snapshots, NetCode also finds the oldest snapshot it applied to any entity.
-* Once NetCode applies the snapshots, the [PredictedSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedSimulationSystemGroup.html) runs from the oldest tick applied to any entity, to the tick the prediction is targeting.
+* Netcode applies the latest snapshot it received from the server to all predicted entities.
+* While applying the snapshots, Netcode also finds the oldest snapshot it applied to any entity.
+* Once Netcode applies the snapshots, the [PredictedSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedSimulationSystemGroup.html) runs from the oldest tick applied to any entity, to the tick the prediction is targeting.
* When the prediction runs, the `PredictedSimulationSystemGroup` sets the correct time for the current prediction tick in the ECS TimeData struct. It also sets the `ServerTick` in the [NetworkTime](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkTime.html) singleton to the tick being predicted.
-Because the prediction loop runs from the oldest tick applied to any entity, and some entities might already have newer data, you must check whether each entity needs to be simulated or not. To perform these checks, either add `.WithAll()` or call the static method [PredictedGhostComponent.ShouldPredict](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedGhostComponent.html#Unity_NetCode_PredictedGhostComponent_ShouldPredict_System_UInt32_) before updating an entity. If it returns `false` the update should not run for that entity.
+Because the prediction loop runs from the oldest tick applied to any entity, and some entities might already have newer data, **you must check whether each entity needs to be simulated or not**. There are two distinct wayw
+to do this check:
+
+### Check which entities to predict using the Simulate tag component (PREFERRED)
+The client use the `Simulate` tag, present on all entities in world, to set when a ghost entity should be predicted or not.
+- At the beginning of the prediction loop, the `Simulate` tag is disabled the simulation of all `Predicted` ghosts.
+- For each prediction tick, the `Simulate` tag is enabled for all the entities that should be simulate for that tick.
+- At the end of the prediction loop, all predicted ghost entities `Simulate` components are guarantee to be enabled.
+
+In your systems that run in the `PredictedSimulationSystemGroup` (or any of its sub-groups) you should add to your queries, EntitiesForEach (deprecated) and idiomatic foreach a `.WithAll<Simulate>>` condition. This will automatically give to the job (or function) the correct set of entities you need to work on.
+
+For example:
+
+```c#
+
+Entities
+ .WithAll()
+ .ForEach(ref Translation trannslation)
+{
+ ///Your update logic here
+}
+```
+
+### Check which entities to predict using the PredictedGhostComponent.ShouldPredict helper method
+The old way To perform these checks, calling the static method [PredictedGhostComponent.ShouldPredict](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedGhostComponent.html#Unity_NetCode_PredictedGhostComponent_ShouldPredict_System_UInt32_) before updating an entity
+is still supported. In this case the method/job that update the entity should looks something like this:
+
+```c#
+
+var serverTick = GetSingleton().ServerTick;
+Entities
+ .WithAll()
+ .ForEach(ref Translation trannslation)
+{
+ if!(PredictedGhostComponent.ShouldPredict(serverTick))
+ return;
+
+ ///Your update logic here
+}
+```
If an entity did not receive any new data from the network since the last prediction ran, and it ended with simulating a full tick (which is not always true when you use a dynamic timestep), the prediction continues from where it finished last time, rather than applying the network data.
## Server
-On the server the prediction loop always runs exactly once, and does not update the TimeData struct because it is already correct. The `ServerTick` in the `NetworkTime` singleton also has the correct value, so the exact same code can be run on both the client and server.
+On the server the prediction loop always runs exactly once, and does not update the TimeData struct because it is already correct.
+The `ServerTick` in the `NetworkTime` singleton also has the correct value, so the exact same code can be run on both the client and server.
+
+The `PredictedGhostComponent.ShouldPredict` always return true when called on the server. The `Simulate` component is also always enabled. You can write the same code for the system that run in prediction, without
+making any distinction if it runs on the server or the client.
## Remote Players Prediction
If commands are configured to be serialized to the other players (see [GhostSnapshots](ghost-snapshots.md#icommandData-serialization)) it is possible to use client-side prediction for the remote players using the remote players commands, the same way you do for the local player.
@@ -42,6 +87,23 @@ the `Simulate` component.
}).Run();
}
```
+
+### Remote player prediction with the new IInputComponentData
+By using the new `IInputComponentData`, you don't need to check or retrieve the input buffer anymore. Your input data for
+the current simulated tick will provide for you.
+
+```c#
+ protected override void OnUpdate()
+ {
+ Entities
+ .WithAll()
+ .ForEach((Entity entity, ref Translation translation, in MyInput input) =>
+ {
+ ///Your update logic here
+ }).Run();
+ }
+```
+
# Prediction Smoothing
Prediction errors are always presents for many reason: slightly different logic in between clients and server, packet drops, quantization errors etc.
For predicted entities the net effect is that when we rollback and predict again from the latest available snapshot, more or large delta in between the recomputed values and the current predicted one can be present.
diff --git a/Documentation~/rpcs.md b/Documentation~/rpcs.md
index 967d526..9aa94c5 100644
--- a/Documentation~/rpcs.md
+++ b/Documentation~/rpcs.md
@@ -1,8 +1,11 @@
# RPCs
-NetCode uses a limited form of RPCs to handle events. A job on the sending side can issue RPCs, and they then execute on a job on the receiving side. This limits what you can do in an RPC; such as what data you can read and modify, and what calls you are allowed to make from the engine. For more information on the Job System see the Unity User Manual documentation on the [C# Job System](https://docs.unity3d.com/2019.3/Documentation/Manual/JobSystem.html).
+Netcode uses a limited form of RPCs to handle events. A job on the sending side can issue RPCs, and they then execute on a job on the receiving side.
+This limits what you can do in an RPC; such as what data you can read and modify, and what calls you are allowed to make from the engine.
+For more information on the Job System see the Unity User Manual documentation on the [C# Job System](https://docs.unity3d.com/2019.3/Documentation/Manual/JobSystem.html).
-To make the system a bit more flexible, you can use the flow of creating an entity that contains specific netcode components such as `SendRpcCommandRequestComponent` and `ReceiveRpcCommandRequestComponent`, which this page outlines.
+To make the system a bit more flexible, you can use the flow of creating an entity that contains specific netcode components such as
+`SendRpcCommandRequestComponent` and `ReceiveRpcCommandRequestComponent`, which this page outlines.
## Extend IRpcCommand
@@ -26,9 +29,11 @@ public struct OurRpcCommand : IRpcCommand
This will generate all the code you need for serialization and deserialization as well as registration of the RPC.
-## Sending and recieving commands
+## Sending and receiving commands
-To complete the example, you must create some entities to send and recieve the commands you created. To send the command you need to create an entity and add the command and the special component [SendRpcCommandRequestComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.SendRpcCommandRequestComponent.html) to it. This component has a member called `TargetConnection` that refers to the remote connection you want to send this command to.
+To complete the example, you must create some entities to send and recieve the commands you created.
+To send the command you need to create an entity and add the command and the special component [SendRpcCommandRequestComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.SendRpcCommandRequestComponent.html) to it.
+This component has a member called `TargetConnection` that refers to the remote connection you want to send this command to.
> [!NOTE]
> If `TargetConnection` is set to `Entity.Null` you will broadcast the message. On a client you don't have to set this value because you will only send to the server.
@@ -38,7 +43,7 @@ The following is an example of a simple send system:
```c#
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
-public class ClientRpcSendSystem : ComponentSystem
+public class ClientRpcSendSystem : SystemBase
{
protected override void OnCreate()
{
@@ -49,9 +54,7 @@ public class ClientRpcSendSystem : ComponentSystem
{
if (Input.GetKey("space"))
{
- var req = PostUpdateCommands.CreateEntity();
- PostUpdateCommands.AddComponent(req, new OurRpcCommand());
- PostUpdateCommands.AddComponent(req, new SendRpcCommandRequestComponent());
+ EntityManager.CreateEntity(typeof(OurRpcCommand), typeof(SendRpcCommandRequestComponent));
}
}
}
@@ -63,7 +66,7 @@ When the rpc is received, an entity that you can filter on is created by a code-
```c#
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
-public class ServerRpcReceiveSystem : ComponentSystem
+public class ServerRpcReceiveSystem : SystemBase
{
protected override void OnUpdate()
{
@@ -71,7 +74,7 @@ public class ServerRpcReceiveSystem : ComponentSystem
{
PostUpdateCommands.DestroyEntity(entity);
Debug.Log("We received a command!");
- });
+ }).Run();
}
}
```
@@ -208,7 +211,12 @@ public struct OurDataRpcCommand : IComponentData, IRpcCommandSerializer().GetRpcQueue();`. You can either call it in `OnUpdate` or call it in `OnCreate` and cache the value through the lifetime of your application. If you do call it in `OnCreate` you must make sure that the system calling it is created after `RpcSystem`. When you have the queue, get the `OutgoingRpcDataStreamBufferComponent` from an entity to schedule events in the queue and then call `rpcQueue.Schedule(rpcBuffer, new OurRpcCommand);`, as follows:
+The [RpcQueue](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.RpcQueue-1.html) is used internally to schedule outgoing RPCs.
+However, you can manually create your own queue and use it to schedule RPCs.
+To do this, call `GetSingleton().GetRpcQueue();`. You can either call it in `OnUpdate` or call it in `OnCreate` and cache the value through the lifetime of your application.
+If you do call it in `OnCreate` you must make sure that the system calling it is created after `RpcSystem`.
+
+When you have the queue, get the `OutgoingRpcDataStreamBufferComponent` from an entity to schedule events in the queue and then call `rpcQueue.Schedule(rpcBuffer, new OurRpcCommand);`, as follows:
```c#
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
diff --git a/Documentation~/synchronization.md b/Documentation~/synchronization.md
new file mode 100644
index 0000000..adb8681
--- /dev/null
+++ b/Documentation~/synchronization.md
@@ -0,0 +1,8 @@
+
+# Synchronization Concepts
+
+| **Topic** | **Description** |
+|:------------------------------------------------|:----------------------------------------------|
+| **[Ghost Synchronization](ghost-snapshots.md)** | Describes Ghost Synchronization and Snapshots |
+| **[Ghost Spawning](ghost-spawning.md)** | Describes how to spawn ghost entities |
+| **[Commands](command-stream.md)** | Describes the Command Stream Synchronization |
diff --git a/Documentation~/time-synchronization.md b/Documentation~/time-synchronization.md
index e5da67e..33a159c 100644
--- a/Documentation~/time-synchronization.md
+++ b/Documentation~/time-synchronization.md
@@ -1,15 +1,72 @@
-## Time synchronization
+# Time synchronization
-NetCode uses a server authoritative model, which means that the server executes a fixed time step based on how much time has passed since the last update. As such, the client needs to match the server time at all times for the model to work.
+Netcode uses a server authoritative model, which means that the server executes a fixed time step based on how much time has passed since the last update.
+As such, the client needs to match the server time at all times for the model to work.
-[NetworkTimeSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkTimeSystem.html) calculates which server time to present on the client. The network time system calculates an initial estimate of the server time based on the round trip time and latest received snapshot from the server. When the client receives an initial estimate, it makes small changes to the time progress rather than doing large changes to the current time. To make accurate adjustments, the server tracks how long it keeps commands in a buffer before it uses them. This is sent back to the client and the client adjusts its time so it receives commands just before it needs them.
+## The NetworkTimeSystem
-The client sends commands to the server. The commands will arrive at some point in the future. When the server receives these commands, it uses them to run the game simulation. The client needs to estimate which tick this is going to happen on the server and present that, otherwise the client and server apply the inputs at different ticks. The tick the client estimates the server will apply the commands on is called the **prediction tick**. You should only use prediction time for a predicted object like the local player.
+[NetworkTimeSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkTimeSystem.html) calculates which server time to present on the client.
+The network time system calculates an initial estimate of the server time based on the round trip time and latest received snapshot from the server.
+When the client receives an initial estimate, it makes small changes to the time progress rather than doing large changes to the current time.
+To make accurate adjustments, the server tracks how long it keeps commands in a buffer before it uses them.
+This is sent back to the client and the client adjusts its time so it receives commands just before it needs them.
-For interpolated objects, the client should present them in a state it has received data for. This time is called **interpolation**, and Unity calculates it as a time offset from the prediction time. The offset is called **prediction delay** and Unity slowly adjusts it up and down in small increments to keep the interpolation time advancing at a smooth rate. Unity calculates the interpolation delay from round trip time and jitter so that the data is generally available. The delay adds additional time based on the network tick rate to make sure it can handle a packet being lost. You can visualize the time offsets and scales in this section in the graphs in the snapshot visualization tool, [NetDbg](ghost-snapshots#Snapshot-visualization-tool).
+The client sends commands to the server. The commands will arrive at some point in the future. When the server receives these commands, it uses them to run the game simulation.
+The client needs to estimate which tick this is going to happen on the server and present that, otherwise the client and server apply the inputs at different simulation step.
+
+The tick the client estimates the server will apply the commands on is called the **prediction tick**. You should only use prediction time for a predicted object like the local player.
+
+For interpolated objects, the client should present them in a state it has received data for. This time is called **interpolation tick**. The `interpolation tick` is calculated as an offset in respect the `predicted tick`.
+That time offset is called **prediction delay**.
+The `interpolation delay` is calculated by taking into account round trip time, jitter and packet arrival rate, all data that is generally available on the client.
+We also add some additional time, based on the network tick rate, to make sure we can handle some packets being lost. You can visualize the time offsets and scales in the snapshot visualization tool, [NetDbg](ghost-snapshots#Snapshot-visualization-tool).
+
+The `NetworkTimeSystem` slowly adjusts both `prediction tick` and `interpolation delay` in small increments to keep them advancing at a smooth rate and ensure that neither the
+interpolation tick nor the prediction tick goes back in time.
### Configuring clients interpolation
-A __ClientTickRate__ singleton entity in the client World can be used to configure the interpolation times used by the client:
-*__InterpolationTimeMS__ - if different than 0, override the interpolation time tick used to interpolate the ghosts.
-*__MaxExtrapolationTimeSimTicks__ - the maximum time in simulation ticks which the client can extrapolate ahead when data is missing.
-It is possible to futher customize the client times calculation. Please read the [ClientTickRate](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientTickRate.html) documentation for more in depth information
+A [ClientTickRate](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientTickRate.html) singleton entity in the client World can be used to
+configure how the system estimate both prediction tick and interpolation delay.
+
+
+| Paramater | |
+|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| InterpolationTimeNetTicks | The number of simulation tick to use as an interpolation buffer for interpolated ghosts. |
+| MaxExtrapolationTimeSimTicks | The maximum time in simulation ticks which the client can extrapolate ahead when data is missing |
+| MaxPredictAheadTimeMS | This is the maximum accepted ping, rtt will be clamped to this value when calculating server tick on the client, which means if ping is higher than this the server will get old commands.
Increasing this makes the client able to deal with higher ping, but the client needs to run more prediction steps which takes more CPU time |
+| TargetCommandSlack | Specifies the number of simulation ticks the client tries to make sure the commands are received by the server before they are used on the server. |
+
+It is possible to further customize the client times calculation. Please read the [ClientTickRate](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientTickRate.html) documentation for more in depth information.
+
+## Retrieving timing information in your application
+Netcode for Entities provide a [NetworkTime](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkTime.html) singleton
+that should be used to retrieve the current simulated/predicted server tick, interpolated tick and other time related properties.
+
+```csharp
+var networkTime = SystemAPI.GetSingleton();
+var currentTick = networkTime.ServerTick;
+...
+```
+
+The `NetworkTime` can be used indistinctly on both client and server both inside and outside the prediction loop.
+For the prediction loop in particular, the `NetworkTime` add some flags to the current simulated tick that can be used to implement certain logic:
+For example:
+- IsFirstPredictionTick : the current server tick is the first one we are predict from the last received snapshot for that entity.
+- IsFinalPredictionTick : the current server tick which will be the last tick to predict.
+- IsFirstTimeFullyPredictingTick: the current server tick is a full tick and this is the first time it is being predicting as a non-partial tick. Useful to implement actions that should be executed only once.
+
+And many others. Please check [NetworkTime docs](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkTime.html) for further information.
+
+## Client DeltaTime, ElapsedTime and Unscaled time
+When the client connect to the server, the elapsed `DeltaTime`, and total `ElapsedTime` are handled differently.
+That because the needs for the client to keep the predicted tick in sync with the server; The application perceived `DeltaTime` is scaled up and down, to accelerate or slowdown the simulation.
+
+The time scaling has some implication:
+- **For all systems updating inside the `SimulationSystemGroup`** (and sub-groups) the `Time.DeltaTime` and the `Time.ElapsedTime` will reflects this scaled elapsed time.
+- For systems updating in the `PresentationSystemGroup` or `InitializationSystemGroup`, or in general outside the `SimulationSystemGroup`, the reported timing are the one normally reported by the application loop.
+
+Because of that, the `Time.ElapsedTime` seen inside and outside the simulation group is usually different.
+
+For cases where you need to have access to real, unscaled delta and elapsed time inside the `SimulationSystemGroup`, you can use the
+[UnscaledClientTime](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkTime.html) singleton.
+The values in the `UnscaledClientTime.DeltaTime` and `UnscaledClientTime.ElapsedTime` are the ones normally reported by application loop.
diff --git a/Editor/Authoring/BakedNetCodeComponents.cs b/Editor/Authoring/BakedNetCodeComponents.cs
index 62d23c5..e088008 100644
--- a/Editor/Authoring/BakedNetCodeComponents.cs
+++ b/Editor/Authoring/BakedNetCodeComponents.cs
@@ -36,32 +36,28 @@ class BakedComponentItem
public BakedEntityResult EntityParent;
public string fullname;
public Type managedType;
- public GhostComponentAttribute ghostComponentAttribute;
- /// Fallback is to use the managed type if not found.
- public VariantType variant;
- public VariantType[] availableVariants;
- public string[] availableVariantReadableNames;
+ /// Determined by the ComponentOverride (fallback is ).
+ public ComponentTypeSerializationStrategy serializationStrategy;
+ /// Cache the default variant so we can mark it up as such in the Inspection UI.
+ public ComponentTypeSerializationStrategy defaultSerializationStrategy;
+ /// Lists all strategies available to this baked component.
+ public ComponentTypeSerializationStrategy[] availableSerializationStrategies;
+ public string[] availableSerializationStrategyDisplayNames;
public int entityIndex;
public EntityGuid entityGuid;
public bool anyVariantIsSerialized;
- public CodeGenTypeMetaData metaData;
- /// Cache the default variant so we can mark it up as such in the Inspection UI.
- public VariantType defaultVariant;
-
- public bool isDontSerializeVariant => variant.Hash == GhostVariantsUtility.DontSerializeHash;
-
public GhostPrefabType PrefabType => HasPrefabOverride() && GetPrefabOverride().IsPrefabTypeOverriden
? GetPrefabOverride().PrefabType
: DefaultPrefabType;
/// Note that variant.PrefabType has higher priority than attribute.PrefabType.
- GhostPrefabType DefaultPrefabType => variant.PrefabType != GhostPrefabType.All ? variant.PrefabType : ghostComponentAttribute.PrefabType;
+ GhostPrefabType DefaultPrefabType => serializationStrategy.PrefabType != GhostPrefabType.All ? serializationStrategy.PrefabType : defaultSerializationStrategy.PrefabType;
public GhostSendType SendTypeOptimization => HasPrefabOverride() && GetPrefabOverride().IsSendTypeOptimizationOverriden
? GetPrefabOverride().SendTypeOptimization
- : ghostComponentAttribute.SendTypeOptimization;
+ : defaultSerializationStrategy.SendTypeOptimization;
public ulong VariantHash
{
@@ -78,23 +74,23 @@ public ulong VariantHash
}
///
- /// Denotes if this type supports user modification of .
+ /// Denotes if this type supports user modification of .
/// We obviously support it "implicitly" if we have multiple variant types.
///
- public bool DoesAllowVariantModification => !metaData.HasDontSupportPrefabOverridesAttribute && (metaData.HasSupportsPrefabOverridesAttribute || HasMultipleVariants);
+ public bool DoesAllowVariantModification => serializationStrategy.HasDontSupportPrefabOverridesAttribute == 0 && (serializationStrategy.HasSupportsPrefabOverridesAttribute != 0 || HasMultipleVariants);
///
/// Denotes if this type supports user modification of .
///
- public bool DoesAllowSendTypeOptimizationModification => !metaData.HasDontSupportPrefabOverridesAttribute && anyVariantIsSerialized && variant.Source != VariantType.VariantSource.ManualDontSerializeVariant && EntityParent.GoParent.RootAuthoring.SupportsSendTypeOptimization;
+ public bool DoesAllowSendTypeOptimizationModification => serializationStrategy.HasDontSupportPrefabOverridesAttribute == 0 && anyVariantIsSerialized && !serializationStrategy.IsDontSerializeVariant && EntityParent.GoParent.RootAuthoring.SupportsSendTypeOptimization;
///
/// Denotes if this type supports user modification of .
///
- public bool DoesAllowPrefabTypeModification => !metaData.HasDontSupportPrefabOverridesAttribute && metaData.HasSupportsPrefabOverridesAttribute;
+ public bool DoesAllowPrefabTypeModification => serializationStrategy.HasDontSupportPrefabOverridesAttribute == 0 && serializationStrategy.HasSupportsPrefabOverridesAttribute != 0;
/// I.e. Implicitly supports prefab overrides.
- internal bool HasMultipleVariants => availableVariants.Length > 1;
+ internal bool HasMultipleVariants => availableSerializationStrategies.Length > 1;
/// Returns by ref. Throws if not found. Use .
public ref GhostAuthoringInspectionComponent.ComponentOverride GetPrefabOverride()
@@ -113,7 +109,7 @@ public bool HasPrefabOverride()
/// Returns the current override if it exists, or a new one, by ref.
public ref GhostAuthoringInspectionComponent.ComponentOverride GetOrAddPrefabOverride()
{
- var setPrefabType = (variant.PrefabType != GhostPrefabType.All);
+ var setPrefabType = (serializationStrategy.PrefabType != GhostPrefabType.All);
var defaultPrefabType = setPrefabType ? DefaultPrefabType : (GhostPrefabType)GhostAuthoringInspectionComponent.ComponentOverride.NoOverride;
EntityParent.GoParent.SourceInspection.GetOrAddPrefabOverride(managedType, entityGuid, defaultPrefabType, out bool created);
ref var @override = ref GetPrefabOverride();
@@ -128,10 +124,10 @@ public bool HasPrefabOverride()
///
public void SaveVariant(bool warnIfChosenIsNotAlreadySaved, bool allowSettingDefaultToRevertOverride)
{
- if (variant.Hash != 0 && !VariantIsTheDefault && !HasPrefabOverride())
+ if (serializationStrategy.Hash != 0 && !VariantIsTheDefault && !HasPrefabOverride())
{
if(warnIfChosenIsNotAlreadySaved)
- Debug.LogError($"Discovered on ghost '{EntityParent.GoParent.SourceGameObject.name}' that in-use variant ({variant}) was not saved as a prefabOverride! Fixed.");
+ Debug.LogError($"Discovered on ghost '{EntityParent.GoParent.SourceGameObject.name}' that in-use variant ({serializationStrategy}) was not saved as a prefabOverride! Fixed.");
GetOrAddPrefabOverride();
}
@@ -139,22 +135,16 @@ public void SaveVariant(bool warnIfChosenIsNotAlreadySaved, bool allowSettingDef
if (HasPrefabOverride())
{
ref var @override = ref GetPrefabOverride();
- var hash = allowSettingDefaultToRevertOverride && VariantIsTheDefault ? 0 : variant.Hash;
+ var hash = (!@override.IsVariantOverriden || allowSettingDefaultToRevertOverride) && VariantIsTheDefault ? 0 : serializationStrategy.Hash;
if (@override.VariantHash != hash)
{
@override.VariantHash = hash;
- EntityParent.GoParent.SourceInspection.SavePrefabOverride(ref @override, $"Confirmed Variant on {fullname} is {variant}");
+ EntityParent.GoParent.SourceInspection.SavePrefabOverride(ref @override, $"Confirmed Variant on {fullname} is {serializationStrategy}");
}
}
-
- // Prioritize fetching the GhostComponentAttribute from the variant (if we have one),
- // otherwise fallback to the "main" type (which is already set).
- var attributeOnVariant = variant.Variant.GetCustomAttribute();
- if (attributeOnVariant != null)
- ghostComponentAttribute = attributeOnVariant;
}
- internal bool VariantIsTheDefault => variant.Hash == defaultVariant.Hash;
+ internal bool VariantIsTheDefault => serializationStrategy.Hash == defaultSerializationStrategy.Hash;
/// Note that this is an "override" action. Reverting to default is a different action.
public void TogglePrefabType(GhostPrefabType type)
@@ -177,7 +167,7 @@ public void RemoveEntirePrefabOverride(DropdownMenuAction action)
{
if (HasPrefabOverride())
{
- variant = defaultVariant;
+ serializationStrategy = defaultSerializationStrategy;
ref var @override = ref GetPrefabOverride();
@override.Reset();
SaveVariant(false, true);
@@ -210,11 +200,11 @@ public void ResetVariantToDefault()
{
if (HasPrefabOverride())
{
- variant = defaultVariant;
+ serializationStrategy = defaultSerializationStrategy;
SaveVariant(false, true);
}
}
- public override string ToString() => $"BakedComponentItem[{fullname} with {variant}, {availableVariants.Length} variants available, entityGuid: {entityGuid}]";
+ public override string ToString() => $"BakedComponentItem[{fullname} with {serializationStrategy}, {availableSerializationStrategies.Length} variants available, entityGuid: {entityGuid}]";
}
}
diff --git a/Editor/Authoring/EntityPrefabComponentsPreview.cs b/Editor/Authoring/EntityPrefabComponentsPreview.cs
index 7797f56..fb05e3e 100644
--- a/Editor/Authoring/EntityPrefabComponentsPreview.cs
+++ b/Editor/Authoring/EntityPrefabComponentsPreview.cs
@@ -124,21 +124,22 @@ BakedEntityResult CreateBakedEntityResult(BakedGameObjectResult parent, int enti
IsRoot = isRoot,
};
- var collectionData = world.GetExistingSystemManaged().ghostComponentSerializerCollectionDataCache;
+ using var query = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly());
+ var collectionData = query.GetSingleton();
AddToComponentList(result, result.BakedComponents, in collectionData, world, convertedEntity, entityIndex);
- var variantTypesList = new NativeList(4, Allocator.Temp);
+ var variantTypesList = new NativeList(4, Allocator.Temp);
foreach (var compItem in result.BakedComponents)
{
var searchHash = compItem.VariantHash;
variantTypesList.Clear();
- for (int i = 0; i < compItem.availableVariants.Length; i++)
+ for (int i = 0; i < compItem.availableSerializationStrategies.Length; i++)
{
- variantTypesList.Add(compItem.availableVariants[i]);
+ variantTypesList.Add(compItem.availableSerializationStrategies[i]);
}
- compItem.variant = collectionData.GetCurrentVariantTypeForComponent(ComponentType.ReadWrite(compItem.managedType), searchHash, variantTypesList, isRoot);
+ compItem.serializationStrategy = collectionData.SelectSerializationStrategyForComponentWithHash(ComponentType.ReadWrite(compItem.managedType), searchHash, variantTypesList, isRoot);
if (compItem.anyVariantIsSerialized)
{
@@ -171,21 +172,26 @@ static void AddToComponentList(BakedEntityResult parent, List(convertedEntity);
- using var availableVariants = collectionData.GetAllAvailableVariantsForType(managedType, parent.IsRoot);
- var metaData = collectionData.GetOrCreateMetaData(managedType);
- var canSerializeInAtLeastOneVariant = GhostComponentSerializerCollectionData.AnyVariantsAreSerialized(in availableVariants);
- var defaultVariant = collectionData.GetCurrentVariantTypeForComponent(componentType, 0, availableVariants, parent.IsRoot);
+ using var availableSs = collectionData.GetAllAvailableSerializationStrategiesForType(managedType, parent.IsRoot);
+ var canSerializeInAtLeastOneVariant = GhostComponentSerializerCollectionData.AnyVariantsAreSerialized(in availableSs);
+ var defaultVariant = collectionData.SelectSerializationStrategyForComponentWithHash(componentType, 0, availableSs, parent.IsRoot);
- var readableNames = new string[availableVariants.Length];
- for (var j = 0; j < availableVariants.Length; j++)
+ // Remove test variants as they cannot be selected:
+ for (var j = availableSs.Length - 1; j >= 0; j--)
{
- var vt = availableVariants[j];
+ var ss = availableSs[j];
+ if(ss.IsTestVariant != 0)
+ availableSs.RemoveAt(j);
+ }
- var readableName = vt.CreateReadableName(metaData);
+ // Cache the availableVariants names.
+ var ssDisplayNames = new string[availableSs.Length];
+ for (var j = 0; j < availableSs.Length; j++)
+ {
+ var vt = availableSs[j];
+ ssDisplayNames[j] = vt.DisplayName.ToString();
if (vt.Hash == defaultVariant.Hash)
- readableName += " (Default)";
-
- readableNames[j] = readableName;
+ ssDisplayNames[j] += " (Default)";
}
var componentItem = new BakedComponentItem
@@ -195,12 +201,10 @@ static void AddToComponentList(BakedEntityResult parent, List() ?? new GhostComponentAttribute(),
- availableVariants = availableVariants.ToArrayNBC(),
- availableVariantReadableNames = readableNames,
+ availableSerializationStrategies = availableSs.ToArrayNBC(),
+ availableSerializationStrategyDisplayNames = ssDisplayNames,
anyVariantIsSerialized = canSerializeInAtLeastOneVariant,
- metaData = metaData,
- defaultVariant = defaultVariant,
+ defaultSerializationStrategy = defaultVariant,
};
newComponents.Add(componentItem);
}
diff --git a/Editor/Authoring/GhostAuthoringEditor.uss b/Editor/Authoring/GhostAuthoringEditor.uss
index 16c01a4..632cd37 100644
--- a/Editor/Authoring/GhostAuthoringEditor.uss
+++ b/Editor/Authoring/GhostAuthoringEditor.uss
@@ -34,7 +34,7 @@
.ghost-inspection-entity-content
{
margin-left: 0px;
- padding: 5 10 5 20;
+ padding: 5px 10px 5px 20px;
border-color: #818181;
border-width: 1.5px;
diff --git a/Editor/Authoring/GhostAuthoringInspectionComponentEditor.cs b/Editor/Authoring/GhostAuthoringInspectionComponentEditor.cs
index 0e002cb..e7779b8 100644
--- a/Editor/Authoring/GhostAuthoringInspectionComponentEditor.cs
+++ b/Editor/Authoring/GhostAuthoringInspectionComponentEditor.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Unity.Entities.Conversion;
using Unity.Entities.Editor;
using UnityEditor;
using UnityEditor.SceneManagement;
@@ -194,7 +195,7 @@ void RebuildWindow()
// Warn about replicating child components:
if (!bakedEntityResult.IsRoot)
{
- if (replicated.Any(x => x.variant.IsSerialized))
+ if (replicated.Any(x => x.serializationStrategy.IsSerialized != 0))
{
replicatedContainer.contentContainer.Add(new HelpBox("Note: Serializing child entities is relatively slow. " +
"Prefer to have multiple Ghosts with faked parenting, if possible.", HelpBoxMessageType.Warning));
@@ -323,22 +324,22 @@ static VisualElement CreateVariantDropdown(BakedComponentItem bakedComponent)
};
DropdownStyle(dropdown);
- for (var i = 0; i < bakedComponent.availableVariants.Length; i++)
+ for (var i = 0; i < bakedComponent.availableSerializationStrategies.Length; i++)
{
- dropdown.choices.Add(bakedComponent.availableVariantReadableNames[i]);
+ dropdown.choices.Add(bakedComponent.availableSerializationStrategyDisplayNames[i]);
}
// Set current value:
{
- var index = Array.FindIndex(bakedComponent.availableVariants, x => x.Hash == bakedComponent.variant.Hash);
+ var index = Array.FindIndex(bakedComponent.availableSerializationStrategies, x => x.Hash == bakedComponent.serializationStrategy.Hash);
if (index >= 0)
{
- var selectedVariantName = bakedComponent.availableVariantReadableNames[index];
+ var selectedVariantName = bakedComponent.availableSerializationStrategyDisplayNames[index];
dropdown.SetValueWithoutNotify(selectedVariantName);
}
else
{
- dropdown.SetValueWithoutNotify($"!! Unknown Variant Hash {bakedComponent.VariantHash} !! (Fallback: {bakedComponent.variant.CreateReadableName(bakedComponent.metaData)})");
+ dropdown.SetValueWithoutNotify($"!! Unknown Variant Hash {bakedComponent.VariantHash} !! (Fallback: {bakedComponent.serializationStrategy.DisplayName.ToString()})");
dropdown.style.backgroundColor = GhostAuthoringComponentEditor.brokenColor;
}
}
@@ -346,10 +347,10 @@ static VisualElement CreateVariantDropdown(BakedComponentItem bakedComponent)
// Handle value changed.
dropdown.RegisterValueChangedCallback(evt =>
{
- var indexOf = Array.IndexOf(bakedComponent.availableVariantReadableNames, evt.newValue);
+ var indexOf = Array.IndexOf(bakedComponent.availableSerializationStrategyDisplayNames, evt.newValue);
if (indexOf >= 0)
{
- bakedComponent.variant = bakedComponent.availableVariants[indexOf];
+ bakedComponent.serializationStrategy = bakedComponent.availableSerializationStrategies[indexOf];
bakedComponent.SaveVariant(false, false);
dropdown.style.color = new StyleColor(StyleKeyword.Null);
}
@@ -577,7 +578,7 @@ void ButtonToggled()
}
void UpdateUi()
{
- var defaultValue = (bakedComponent.defaultVariant.PrefabType & type) != 0;
+ var defaultValue = (bakedComponent.defaultSerializationStrategy.PrefabType & type) != 0;
var isSet = (bakedComponent.PrefabType & type) != 0;
button.style.backgroundColor = isSet ? new Color(0.17f, 0.17f, 0.17f) : new Color(0.48f, 0.15f, 0.15f);
diff --git a/Editor/Drawers.meta b/Editor/Drawers.meta
new file mode 100644
index 0000000..6df4964
--- /dev/null
+++ b/Editor/Drawers.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6e80ae9faadec054eba465f3b46b16b7
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Drawers/BoundingBoxDebugGhostDrawerSystem.cs b/Editor/Drawers/BoundingBoxDebugGhostDrawerSystem.cs
new file mode 100644
index 0000000..2da7303
--- /dev/null
+++ b/Editor/Drawers/BoundingBoxDebugGhostDrawerSystem.cs
@@ -0,0 +1,478 @@
+#if UNITY_EDITOR && !UNITY_DOTSRUNTIME && USING_ENTITIES_GRAPHICS
+using System;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.Profiling;
+using Unity.Rendering;
+using Unity.Transforms;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace Unity.NetCode.Samples.Common
+{
+ [UpdateInGroup(typeof(PresentationSystemGroup))]
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ partial class BoundingBoxDebugGhostDrawerClientSystem : SystemBase
+ {
+ const string k_ServerColorKey = "BoundingBoxDebugGhostDrawer_ServerColor";
+ const string k_PredictedClientColorKey = "BoundingBoxDebugGhostDrawer_PredictedClientColor";
+ const string k_InterpolatedClientColorKey = "BoundingBoxDebugGhostDrawer_InterpolatedClientColor";
+ const string k_ServerGhostMarkerScaleKey = "BoundingBoxDebugGhostDrawer_ServerGhostMarkerScale";
+
+ public static Color ServerColor = Color.red;
+ public static Color PredictedClientColor = Color.green;
+ public static Color InterpolatedClientColor = Color.cyan;
+ public static float GhostServerMarkerScale = 500;
+
+ static DebugGhostDrawer.CustomDrawer s_CustomDrawer;
+
+ static ProfilerMarker s_CreateGeometryJobMarker = new(nameof(s_CreateGeometryJobMarker));
+ static ProfilerMarker s_SetMeshesMarker = new(nameof(s_SetMeshesMarker));
+ static ProfilerMarker s_GatherDataMarker = new(nameof(s_GatherDataMarker));
+ static GUIContent s_ServerGhostMarkerScale = new GUIContent("Marker Scale", "Some server entities may not be replicated on the client.\n\nThis option draws a 3D '+' marker over all server ghosts, which will allow you to see any not-yet-replicated ghosts in the Game view.\n\n0 disables marker rendering.");
+ static readonly VertexAttributeDescriptor[] k_VertexAttributeDescriptors = {new VertexAttributeDescriptor(VertexAttribute.Position)};
+
+ Material m_ServerMat;
+ Material m_PredictedClientMat;
+ Material m_InterpolatedClientMat;
+ Mesh m_ServerMesh;
+ Mesh m_PredictedClientMesh;
+ Mesh m_InterpolatedClientMesh;
+ Entity m_ServerMeshRendererEntity;
+ Entity m_ClientPredictedMeshRendererEntity;
+ Entity m_ClientInterpolatedMeshRendererEntity;
+ EntityQuery m_InterpolatedGhostQuery;
+ EntityQuery m_PredictedGhostQuery;
+
+ [RuntimeInitializeOnLoadMethod]
+ [InitializeOnLoadMethod]
+ static void InitializeAndLoad()
+ {
+ s_CustomDrawer = new DebugGhostDrawer.CustomDrawer("Bounding Boxes", 0, OnGuiDrawOptions, EditorSave);
+ DebugGhostDrawer.RegisterDrawAction(s_CustomDrawer);
+
+ EditorLoad();
+ }
+
+ static void EditorLoad()
+ {
+ ServerColor = ColorUtility.TryParseHtmlString(EditorPrefs.GetString(k_ServerColorKey, null), out var serverColor) ? serverColor : ServerColor;
+ PredictedClientColor = ColorUtility.TryParseHtmlString(EditorPrefs.GetString(k_PredictedClientColorKey, null), out var predictedClientColor) ? predictedClientColor : PredictedClientColor;
+ InterpolatedClientColor = ColorUtility.TryParseHtmlString(EditorPrefs.GetString(k_InterpolatedClientColorKey, null), out var interpolatedClientColor) ? interpolatedClientColor : InterpolatedClientColor;
+ GhostServerMarkerScale = EditorPrefs.GetFloat(k_ServerGhostMarkerScaleKey, 1f);
+ }
+
+ public static void EditorSave()
+ {
+ EditorPrefs.SetString(k_ServerColorKey, "#" + ColorUtility.ToHtmlStringRGBA(ServerColor));
+ EditorPrefs.SetString(k_PredictedClientColorKey, "#" + ColorUtility.ToHtmlStringRGBA(PredictedClientColor));
+ EditorPrefs.SetString(k_InterpolatedClientColorKey, "#" + ColorUtility.ToHtmlStringRGBA(InterpolatedClientColor));
+ EditorPrefs.SetFloat(k_ServerGhostMarkerScaleKey, GhostServerMarkerScale);
+ }
+
+
+ static void UpdateEntityMeshDrawer(EntityManager clientEntityManager, bool enabled, Entity renderEntity, Material material, Color color)
+ {
+ var renderEntityExists = clientEntityManager.Exists(renderEntity);
+ if (renderEntityExists)
+ {
+ material.color = color;
+
+ var shouldBeVisible = enabled && color.a > 0f;
+ var isVisible = !clientEntityManager.HasComponent(renderEntity);
+ if (shouldBeVisible != isVisible)
+ {
+ if (shouldBeVisible)
+ clientEntityManager.RemoveComponent(renderEntity);
+ else
+ clientEntityManager.AddComponent(renderEntity);
+ }
+ }
+ }
+
+ ///
+ /// Note that this shader must exist in the build. Add it to the 'Always Included Shaders' list in your project.
+ /// TODO - Make this feature available in builds after the URP/Unlit DOTS_INSTANCING_ON 4.5 error lands.
+ ///
+ static string GetUnlitShader()
+ {
+#if USING_URP
+ return "Universal Render Pipeline/Unlit";
+#elif USING_HDRP
+ return "HDRP/Unlit";
+#else
+ return "Unlit/Color";
+#endif
+ }
+
+ static void OnGuiDrawOptions()
+ {
+ PredictedClientColor = EditorGUILayout.ColorField("Client (Predicted)", PredictedClientColor);
+ InterpolatedClientColor = EditorGUILayout.ColorField("Client (Interpolated)", InterpolatedClientColor);
+ ServerColor = EditorGUILayout.ColorField("Server", ServerColor);
+ GhostServerMarkerScale = EditorGUILayout.Slider(s_ServerGhostMarkerScale, GhostServerMarkerScale, 0, 100);
+ EditorGUILayout.HelpBox("Note that `BoundingBoxDebugGhostDrawerSystem` will only draw entities on client ghosts with a `WorldRenderBounds` component, and on server ghost entities with a `LocalToWorld` component.", MessageType.Info);
+ }
+
+ static void UpdateIndividualMeshOptimized(Mesh mesh, ref NativeList newVerts, ref NativeList newIndices)
+ {
+ const MeshUpdateFlags flags = MeshUpdateFlags.DontValidateIndices | MeshUpdateFlags.DontResetBoneBounds | MeshUpdateFlags.DontRecalculateBounds | MeshUpdateFlags.DontNotifyMeshUsers;
+ using (s_SetMeshesMarker.Auto())
+ {
+ if (mesh.vertexCount < newVerts.Length)
+ mesh.SetVertexBufferParams(RoundTo(newVerts.Length, 2048), k_VertexAttributeDescriptors);
+ mesh.SetVertexBufferData(newVerts.AsArray(), 0, 0, newVerts.Length, 0, flags);
+
+ if (mesh.GetIndexCount(0) < newIndices.Length)
+ mesh.SetIndexBufferParams(RoundTo(newIndices.Length, 8192), IndexFormat.UInt32);
+ mesh.SetIndexBufferData(newIndices.AsArray(), 0, 0, newIndices.Length, flags);
+
+ var smd = new SubMeshDescriptor
+ {
+ topology = MeshTopology.Lines,
+ vertexCount = newVerts.Length,
+ indexCount = newIndices.Length,
+ };
+ mesh.SetSubMesh(0, smd, flags);
+
+
+ mesh.UploadMeshData(false);
+ }
+ }
+
+ /// Rounds up to the next multiplier value (which must be a power of 2) in `multiplier` increments.
+ /// This *linear* approach is better than an exponential (e.g. `math.ceilpow2`), as the latter is far too excessive in allocation (which slows down `Mesh.SetVertexBufferData`).
+ static int RoundTo(int value, int roundToWithPow2) => (value + roundToWithPow2 - 1)&~(roundToWithPow2-1);
+
+ protected override void OnStopRunning()
+ {
+ OnDestroy();
+ }
+
+ protected override void OnDestroy()
+ {
+ UpdateEntityMeshDrawer(EntityManager, false, m_ServerMeshRendererEntity, m_ServerMat, ServerColor);
+ UpdateEntityMeshDrawer(EntityManager, false, m_ClientPredictedMeshRendererEntity, m_PredictedClientMat, PredictedClientColor);
+ UpdateEntityMeshDrawer(EntityManager, false, m_ClientInterpolatedMeshRendererEntity, m_InterpolatedClientMat, InterpolatedClientColor);
+ }
+
+ protected override void OnUpdate()
+ {
+ DebugGhostDrawer.RefreshWorldCaches();
+
+ var enabled = Enabled && s_CustomDrawer.Enabled && DebugGhostDrawer.HasRequiredWorlds;
+ UpdateEntityMeshDrawer(EntityManager, enabled, m_ServerMeshRendererEntity, m_ServerMat, ServerColor);
+ UpdateEntityMeshDrawer(EntityManager, enabled, m_ClientPredictedMeshRendererEntity, m_PredictedClientMat, PredictedClientColor);
+ UpdateEntityMeshDrawer(EntityManager, enabled, m_ClientInterpolatedMeshRendererEntity, m_InterpolatedClientMat, InterpolatedClientColor);
+
+ if (!enabled) return;
+
+ CreateRenderEntitiesIfNull();
+
+ s_GatherDataMarker.Begin();
+
+ Dependency.Complete();
+
+ var serverWorld = DebugGhostDrawer.FirstServerWorld;
+ serverWorld.EntityManager.CompleteAllTrackedJobs();
+
+ var serverSystem = serverWorld.GetOrCreateSystemManaged();
+
+ var numInterpolatedEntities = m_InterpolatedGhostQuery.CalculateEntityCount();
+ var numPredictedEntities = m_PredictedGhostQuery.CalculateEntityCount();
+ var numEntitiesToIterate = numPredictedEntities + numInterpolatedEntities;
+
+ serverSystem.SpawnedGhostEntityMapSingletonQuery.CompleteDependency();
+ var serverSpawnedGhostEntityMap = serverSystem.SpawnedGhostEntityMapSingletonQuery.GetSingleton().Value;
+
+ var serverLocalToWorldMap = serverSystem.LocalToWorldsMapR0;
+ serverLocalToWorldMap.Update(serverSystem);
+
+ var serverVertices = new NativeList(numEntitiesToIterate * 10, Allocator.Temp);
+ var serverIndices = new NativeList(numEntitiesToIterate * 26, Allocator.Temp);
+
+ s_GatherDataMarker.End();
+
+ if (numPredictedEntities > 0)
+ {
+ var predictedClientVertices = new NativeList(numPredictedEntities * 10, Allocator.Temp);
+ var predictedClientIndices = new NativeList(numPredictedEntities * 26, Allocator.Temp);
+
+ using (s_CreateGeometryJobMarker.Auto())
+ {
+ Entities
+ .WithName("CreateGeometryWithPredictedGhosts")
+ .WithStoreEntityQueryInField(ref m_PredictedGhostQuery)
+ .WithReadOnly(serverLocalToWorldMap)
+ .WithReadOnly(serverSpawnedGhostEntityMap)
+ .WithAll()
+ .ForEach((in WorldRenderBounds clientRenderBounds, in GhostComponent ghostComponent) =>
+ {
+ CreateLineGeometryWithGhosts(in clientRenderBounds, in ghostComponent, in serverSpawnedGhostEntityMap, in serverLocalToWorldMap, ref predictedClientVertices, ref predictedClientIndices, ref serverVertices, ref serverIndices);
+ }).Run();
+ }
+
+ UpdateIndividualMeshOptimized(m_PredictedClientMesh, ref predictedClientVertices, ref predictedClientIndices);
+
+ predictedClientVertices.Dispose();
+ predictedClientIndices.Dispose();
+ }
+ else m_PredictedClientMesh.Clear(true);
+
+ if (numInterpolatedEntities > 0)
+ {
+ var interpolatedClientVertices = new NativeList(numInterpolatedEntities * 10, Allocator.Temp);
+ var interpolatedClientIndices = new NativeList(numInterpolatedEntities * 26, Allocator.Temp);
+
+ using (s_CreateGeometryJobMarker.Auto())
+ {
+ Entities
+ .WithName("CreateGeometryWithInterpolatedGhosts")
+ .WithStoreEntityQueryInField(ref m_InterpolatedGhostQuery)
+ .WithReadOnly(serverLocalToWorldMap)
+ .WithReadOnly(serverSpawnedGhostEntityMap)
+ .WithNone()
+ .ForEach((in WorldRenderBounds clientRenderBounds, in GhostComponent ghostComponent) =>
+ {
+ CreateLineGeometryWithGhosts(in clientRenderBounds, in ghostComponent, in serverSpawnedGhostEntityMap, in serverLocalToWorldMap, ref interpolatedClientVertices, ref interpolatedClientIndices, ref serverVertices, ref serverIndices);
+ }).Run();
+ }
+
+ UpdateIndividualMeshOptimized(m_InterpolatedClientMesh, ref interpolatedClientVertices, ref interpolatedClientIndices);
+
+ interpolatedClientVertices.Dispose();
+ interpolatedClientIndices.Dispose();
+ }
+ else m_InterpolatedClientMesh.Clear(true);
+
+ // For all server entities, also draw a cross.
+ if (GhostServerMarkerScale > 0)
+ {
+ using (s_CreateGeometryJobMarker.Auto())
+ {
+ var serverL2Ws = serverSystem.GhostL2WQuery.ToComponentDataArray(Allocator.Temp);
+ var scale = GhostServerMarkerScale * .5f;
+ Job
+ .WithReadOnly(serverL2Ws)
+ .WithDisposeOnCompletion(serverL2Ws)
+ .WithCode(() =>
+ {
+ var x = new float3(scale, 0, 0);
+ var y = new float3(0, scale, 0);
+ var z = new float3(0, 0, scale);
+ serverVertices.Capacity = math.max(serverVertices.Capacity, serverVertices.Length + serverL2Ws.Length * 6);
+ serverIndices.Capacity = math.max(serverIndices.Capacity, serverIndices.Length + serverL2Ws.Length * 6);
+
+ for (var i = 0; i < serverL2Ws.Length; i++)
+ {
+ var pos = serverL2Ws[i].Position;
+ DebugDrawLine(pos - x, pos + x, ref serverVertices, ref serverIndices);
+ DebugDrawLine(pos - y, pos + y, ref serverVertices, ref serverIndices);
+ DebugDrawLine(pos - z, pos + z, ref serverVertices, ref serverIndices);
+ }
+ }).Run();
+ }
+ }
+
+ UpdateIndividualMeshOptimized(m_ServerMesh, ref serverVertices, ref serverIndices);
+
+ serverVertices.Dispose();
+ serverIndices.Dispose();
+ }
+
+ void ClearMeshes()
+ {
+ m_ServerMesh.Clear(true);
+ m_PredictedClientMesh.Clear(true);
+ m_InterpolatedClientMesh.Clear(true);
+ }
+
+ static void CreateLineGeometryWithGhosts(in WorldRenderBounds worldRenderBounds, in GhostComponent ghostComponent, in NativeParallelHashMap.ReadOnly serverSpawnedGhostEntityMap, in ComponentLookup serverLocalToWorldMap, ref NativeList clientVertices, ref NativeList clientIndices, ref NativeList serverVertices, ref NativeList serverIndices)
+ {
+ // Client AABB:
+ var aabb = worldRenderBounds.Value;
+ DebugDrawWireCube(ref aabb, ref clientVertices, ref clientIndices);
+
+ if (serverSpawnedGhostEntityMap.TryGetValue(ghostComponent, out var serverEntity) && serverLocalToWorldMap.TryGetComponent(serverEntity, out var serverL2W))
+ {
+ var serverPos = serverL2W.Position;
+ if (math.distancesq(aabb.Center, serverPos) > 0.002f)
+ {
+ // Server to Client Line:
+ DebugDrawLine(aabb.Center, serverPos, ref serverVertices, ref serverIndices);
+
+ // Server AABB:
+ aabb.Center = serverPos;
+ DebugDrawWireCube(ref aabb, ref serverVertices, ref serverIndices);
+ }
+ }
+ }
+
+ static void DebugDrawLine(float3 a, float3 b, ref NativeList verts, ref NativeList indices)
+ {
+ var length = verts.Length;
+ indices.Add(length);
+ indices.Add(length + 1);
+ verts.Add(a);
+ verts.Add(b);
+ }
+
+ void CreateRenderEntitiesIfNull()
+ {
+ if (EntityManager.Exists(m_ClientInterpolatedMeshRendererEntity)) return;
+
+ m_ServerMesh = CreateMesh(nameof(m_ServerMesh));
+ m_InterpolatedClientMesh = CreateMesh(nameof(m_InterpolatedClientMesh));
+ m_PredictedClientMesh = CreateMesh(nameof(m_PredictedClientMesh));
+
+ var unlitShaderName = GetUnlitShader();
+ var unlitShader = Shader.Find(unlitShaderName);
+ if (unlitShader == null)
+ {
+ unlitShader = QualitySettings.renderPipeline?.defaultMaterial?.shader;
+ Debug.LogError($"{nameof(BoundingBoxDebugGhostDrawerClientSystem)}.{nameof(GetUnlitShader)} was unable to find shader '{unlitShaderName}' for this Render Pipeline. Please ensure it's added to the 'Always Included Shaders' list in your project. Trying to use this RP's default material '{unlitShader}' (assuming it exists).");
+ if (unlitShader == null)
+ {
+ Enabled = false;
+ return;
+ }
+ }
+
+ // Draw client boxes on top of server boxes.
+ m_ServerMat = new Material(unlitShader)
+ {
+ name = m_ServerMesh.name,
+ hideFlags = HideFlags.HideAndDontSave,
+ };
+ m_PredictedClientMat = new Material(unlitShader)
+ {
+ name = m_ServerMesh.name,
+ hideFlags = HideFlags.HideAndDontSave,
+ };
+ m_InterpolatedClientMat = new Material(unlitShader)
+ {
+ name = m_ServerMesh.name,
+ hideFlags = HideFlags.HideAndDontSave,
+ };
+ m_ServerMat.renderQueue = (int)RenderQueue.Overlay + 10;
+ m_InterpolatedClientMat.renderQueue = (int)RenderQueue.Overlay + 11;
+ m_PredictedClientMat.renderQueue = (int) RenderQueue.Overlay + 12;
+
+ m_ServerMeshRendererEntity = EntityManager.CreateEntity(ComponentType.ReadOnly());
+ EntityManager.SetComponentData(m_ServerMeshRendererEntity, new LocalToWorld
+ {
+ Value = float4x4.TRS(new float3(0, 0, 0), quaternion.identity, new float3(1))
+ });
+
+ // See runtime-entity-creation.md for details on how this works.
+ // Note that the Entities Graphics package doesn't currently support an overload for AddComponents that DOESN'T require a custom RenderMeshArray.
+ // https://jira.unity3d.com/browse/PLAT-1272
+ // Ideally we'd register these materials + meshes into BatchRenderGroup and therefore not need a RenderMeshArray SharedComponent.
+ var materials = new[] {m_ServerMat, m_PredictedClientMat, m_InterpolatedClientMat};
+ var meshes = new[] {m_ServerMesh, m_PredictedClientMesh, m_InterpolatedClientMesh};
+ RenderMeshUtility.AddComponents(m_ServerMeshRendererEntity, EntityManager,
+ new RenderMeshDescription(ShadowCastingMode.Off, false, MotionVectorGenerationMode.ForceNoMotion),
+ new RenderMeshArray(materials, meshes),
+ MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));
+
+ m_ClientPredictedMeshRendererEntity = EntityManager.Instantiate(m_ServerMeshRendererEntity);
+ EntityManager.SetComponentData(m_ClientPredictedMeshRendererEntity, MaterialMeshInfo.FromRenderMeshArrayIndices(1, 1));
+
+ m_ClientInterpolatedMeshRendererEntity = EntityManager.Instantiate(m_ServerMeshRendererEntity);
+ EntityManager.SetComponentData(m_ClientInterpolatedMeshRendererEntity, MaterialMeshInfo.FromRenderMeshArrayIndices(2, 2));
+
+ EntityManager.SetName(m_ServerMeshRendererEntity, m_ServerMesh.name);
+ EntityManager.SetName(m_ClientPredictedMeshRendererEntity, m_PredictedClientMesh.name);
+ EntityManager.SetName(m_ClientInterpolatedMeshRendererEntity, m_InterpolatedClientMesh.name);
+ }
+
+ static Mesh CreateMesh(string name)
+ {
+ var mesh = new Mesh
+ {
+ name = name,
+ indexFormat = IndexFormat.UInt32,
+ hideFlags = HideFlags.HideAndDontSave,
+
+ // We do not want to have to constantly recalculate this debug drawer bounds, so set it to a huge value and leave it.
+ bounds = new Bounds(new float3(0), new float3(100_000_000)),
+ };
+ mesh.MarkDynamic();
+ return mesh;
+ }
+
+ [BurstCompile]
+ static unsafe void DebugDrawWireCube(ref AABB aabb, ref NativeList vertices, ref NativeList indices)
+ {
+ var i = vertices.Length;
+ var min = aabb.Min;
+ var max = aabb.Max;
+
+ var newVertices = stackalloc float3[8];
+ newVertices[0] = new float3(min.x, min.y, min.z);
+ newVertices[1] = new float3(min.x, max.y, min.z);
+ newVertices[2] = new float3(min.x, min.y, max.z);
+ newVertices[3] = new float3(min.x, max.y, max.z);
+ newVertices[4] = new float3(max.x, min.y, min.z);
+ newVertices[5] = new float3(max.x, min.y, max.z);
+ newVertices[6] = new float3(max.x, max.y, min.z);
+ newVertices[7] = new float3(max.x, max.y, max.z);
+ vertices.AddRange(newVertices, 8);
+
+ var newIndices = stackalloc int[24];
+ // 4 left to right (x) rows.
+ newIndices[0] = i + 0;
+ newIndices[1] = i + 4;
+ newIndices[2] = i + 1;
+ newIndices[3] = i + 6;
+ newIndices[4] = i + 2;
+ newIndices[5] = i + 5;
+ newIndices[6] = i + 3;
+ newIndices[7] = i + 7;
+ // 4 bottom to top (y) columns.
+ newIndices[8] = i + 0;
+ newIndices[9] = i + 1;
+ newIndices[10] = i + 4;
+ newIndices[11] = i + 6;
+ newIndices[12] = i + 5;
+ newIndices[13] = i + 7;
+ newIndices[14] = i + 5;
+ newIndices[15] = i + 7;
+ // 4 back to front (z) lines.
+ newIndices[16] = i + 0;
+ newIndices[17] = i + 2;
+ newIndices[18] = i + 4;
+ newIndices[19] = i + 5;
+ newIndices[20] = i + 1;
+ newIndices[21] = i + 3;
+ newIndices[22] = i + 6;
+ newIndices[23] = i + 7;
+ indices.AddRange(newIndices, 24);
+ }
+ }
+
+ // TODO - Exposing APIs on systems is an anti-pattern, but there is no clear alternative for 'world to world' communication.
+ [DisableAutoCreation]
+ [UpdateInGroup(typeof(InitializationSystemGroup))]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ partial class BoundingBoxDebugGhostDrawerServerSystem : SystemBase
+ {
+ internal ComponentLookup LocalToWorldsMapR0;
+ internal EntityQuery SpawnedGhostEntityMapSingletonQuery;
+ public EntityQuery GhostL2WQuery;
+
+ protected override void OnCreate()
+ {
+ LocalToWorldsMapR0 = GetComponentLookup(true);
+ SpawnedGhostEntityMapSingletonQuery = GetEntityQuery(ComponentType.ReadOnly());
+ GhostL2WQuery = GetEntityQuery(ComponentType.ReadOnly(), ComponentType.ReadOnly());
+ Enabled = false;
+ }
+
+ protected override void OnUpdate() => throw new InvalidOperationException();
+ }
+}
+#endif
diff --git a/Editor/Drawers/BoundingBoxDebugGhostDrawerSystem.cs.meta b/Editor/Drawers/BoundingBoxDebugGhostDrawerSystem.cs.meta
new file mode 100644
index 0000000..058e3ac
--- /dev/null
+++ b/Editor/Drawers/BoundingBoxDebugGhostDrawerSystem.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 164dbd60586149e99fa8a8ce6a747ae0
+timeCreated: 1643722785
\ No newline at end of file
diff --git a/Editor/Drawers/Unity.NetCode.Editor.Drawers.asmdef b/Editor/Drawers/Unity.NetCode.Editor.Drawers.asmdef
new file mode 100644
index 0000000..f15a794
--- /dev/null
+++ b/Editor/Drawers/Unity.NetCode.Editor.Drawers.asmdef
@@ -0,0 +1,42 @@
+{
+ "name": "Unity.NetCode.Editor.Drawers",
+ "rootNamespace": "",
+ "references": [
+ "GUID:953adc2a6b8b4e3c8df5b728bcd546e9",
+ "GUID:734d92eba21c94caba915361bd5ac177",
+ "GUID:e0cd26848372d4e5c891c569017e11f1",
+ "GUID:d8b63aba1907145bea998dd612889d6b",
+ "GUID:2665a8d13d1b3f18800f46e256720795",
+ "GUID:8819f35a0fc84499b990e90a4ca1911f",
+ "GUID:a5baed0c9693541a5bd947d336ec7659",
+ "GUID:e04e6c86a9f3947eb95fded39f9e60cc",
+ "GUID:7a450cf7ca9694b5a8bfa3fd83ec635a"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": true,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [
+ {
+ "name": "com.unity.entities.graphics",
+ "expression": "0.0",
+ "define": "USING_ENTITIES_GRAPHICS"
+ },
+ {
+ "name": "com.unity.render-pipelines.universal",
+ "expression": "0.0",
+ "define": "USING_URP"
+ },
+ {
+ "name": "com.unity.render-pipelines.high-definition",
+ "expression": "0.0",
+ "define": "USING_HDRP"
+ }
+ ],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Editor/Drawers/Unity.NetCode.Editor.Drawers.asmdef.meta b/Editor/Drawers/Unity.NetCode.Editor.Drawers.asmdef.meta
new file mode 100644
index 0000000..78a877b
--- /dev/null
+++ b/Editor/Drawers/Unity.NetCode.Editor.Drawers.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 4dd0ec47268cd6c4a95e9a08c89bc23f
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/MultiplayerPlayModeWindow.cs b/Editor/MultiplayerPlayModeWindow.cs
index 21f302c..8133efb 100644
--- a/Editor/MultiplayerPlayModeWindow.cs
+++ b/Editor/MultiplayerPlayModeWindow.cs
@@ -10,6 +10,8 @@
using Unity.Scenes;
using UnityEditor;
using UnityEngine;
+using Unity.Entities.Build;
+using Unity.NetCode.Hybrid;
using Prefs = Unity.NetCode.MultiplayerPlayModePreferences;
namespace Unity.NetCode.Editor
@@ -27,9 +29,8 @@ internal class MultiplayerPlayModeWindow : EditorWindow, IHasCustomMenu
static GUILayoutOption s_NetworkIdWidth = GUILayout.Width(30);
static GUILayoutOption s_WorldNameWidth = GUILayout.Width(130);
- // NWALKER - Update documentation!
static GUIContent s_PlayModeType = new GUIContent("PlayMode Type", "During multiplayer development, it's useful to modify and run the client and server at the same time, in the same process (i.e. \"in-proc\"). DOTS Multiplayer supports this out of the box via the DOTS Entities \"Worlds\" feature.\n\nUse this toggle to determine which mode of operation is used for this playmode session.\n\n\"Client & Server\" is recommended for most workflows.");
- static GUIContent s_ServerWorldDataType = new GUIContent("Server World Data Type", "Select which version of the subscenes you want to load on the server");
+ static GUIContent s_SimulateDedicatedServer = new GUIContent("Simulate Dedicated Server", "When enabled the server will load the same data as a dedicated server, when disabled it will load the same data as a client hosted server.");
static GUIContent s_NumThinClients = new GUIContent("Num Thin Clients", "Thin clients are clients that receive snapshots, but do not attempt to process game logic. They can send arbitrary inputs though, and are useful to simulate opponents (to test connection & game logic).\n\nThin clients are instantiated on boot and at runtime. I.e. This value can be tweaked during playmode.");
static GUIContent s_InstantiationFrequency = new GUIContent("Instantiation Frequency", "How many thin client worlds to instantiate per second. Runtime thin client instantiation can be disabled by setting `RuntimeThinClientWorldInitialization` to null. Does not affect thin clients created during boot.");
@@ -61,7 +62,6 @@ internal class MultiplayerPlayModeWindow : EditorWindow, IHasCustomMenu
static GUIContent[] s_InUseSimulatorPresetContents;
static List s_InUseSimulatorPresetsCache = new List(32);
- static GUIContent[] s_ServerWorldDataStrings = {new GUIContent("Server", ""), new GUIContent("Client & Server", "")};
static readonly GUIContent[] k_PlayModeStrings = { new GUIContent("Client & Server", "Instantiates a server instance alongside a single \"full\" client, with a configurable number of thin clients."), new GUIContent("Client", "Only instantiate a client (with a configurable number of thin clients) that'll automatically attempt to connect to the listed address and port."), new GUIContent("Server", "Only instantiate a server. Expects that clients will be instantiated in another process.")};
static GUILayoutOption s_ExpandWidth = GUILayout.ExpandWidth(true);
static GUILayoutOption s_DontExpandWidth = GUILayout.ExpandWidth(false);
@@ -541,13 +541,12 @@ static void DrawPlayType()
EditorApplication.isPlaying = false;
}
- if (LiveConversionSettings.IsBuiltinBuildsEnabled && (ClientServerBootstrap.PlayType)requestedPlayType == ClientServerBootstrap.PlayType.ClientAndServer)
+ if ((ClientServerBootstrap.PlayType)requestedPlayType != ClientServerBootstrap.PlayType.Client &&
+ ((ClientSettings)DotsGlobalSettings.Instance.ClientProvider).NetCodeClientTarget == NetCodeClientTarget.ClientAndServer)
{
EditorGUI.BeginChangeCheck();
- var requestedServerWorldDataType = (int) Prefs.ServerLoadDataType;
- EditorPopup(s_ServerWorldDataType, s_ServerWorldDataStrings, ref requestedServerWorldDataType);
+ Prefs.SimulateDedicatedServer = EditorGUILayout.Toggle(s_SimulateDedicatedServer, Prefs.SimulateDedicatedServer);
- Prefs.ServerLoadDataType = (MultiplayerPlayModePreferences.ServerWorldDataToLoad) requestedServerWorldDataType;
if (EditorGUI.EndChangeCheck())
{
EditorApplication.isPlaying = false;
@@ -611,7 +610,7 @@ void DrawSimulator()
EditorGUI.BeginChangeCheck();
s_PacketDelayRange.text = $"Range {perPacketMin} to {perPacketMax} (ms)";
- EditorGUILayout.MinMaxSlider(s_PacketDelayRange, ref perPacketMin, ref perPacketMax, 0, 400);
+ EditorGUILayout.MinMaxSlider(s_PacketDelayRange, ref perPacketMin, ref perPacketMax, 0, 500);
if (EditorGUI.EndChangeCheck())
{
// Prevents int precision lost causing this value to change when it shouldn't.
@@ -781,7 +780,7 @@ void DrawClientWorld(World world)
// You can force a timeout even when disconnected, to allow testing reconnect attempts while timed out.
var isTimingOut = conSystem.IsSimulatingTimeout;
- s_Timeout.text = isTimingOut ? $"Simulating Timeout\n[{conSystem.TimeoutSimulationDurationSeconds:n1}s]" : $"Timeout";
+ s_Timeout.text = isTimingOut ? $"Simulating Timeout\n[{Mathf.CeilToInt(conSystem.TimeoutSimulationDurationSeconds):n1}s]" : $"Timeout";
GUI.color = isTimingOut ? GhostAuthoringComponentEditor.brokenColor : Color.white;
if (GUILayout.Button(s_Timeout))
conSystem.ToggleTimeoutSimulation();
@@ -1087,9 +1086,9 @@ protected override void OnCreate()
protected override void OnUpdate()
{
Dependency.Complete();
- var netDebug = GetSingleton();
+ var netDebug = SystemAPI.GetSingleton();
- var unscaledClientTime = GetSingleton();
+ var unscaledClientTime = SystemAPI.GetSingleton();
if (IsSimulatingTimeout)
{
TimeoutSimulationDurationSeconds += unscaledClientTime.UnscaleDeltaTime;
@@ -1107,7 +1106,7 @@ protected override void OnUpdate()
}
}
- ref var netStream = ref GetSingletonRW().ValueRW;
+ ref var netStream = ref SystemAPI.GetSingletonRW().ValueRW;
ref var driverStore = ref netStream.DriverStore;
LastEndpoint = netStream.LastEndPoint;
IsAnyUsingSimulator = driverStore.IsAnyUsingSimulator;
@@ -1130,7 +1129,7 @@ protected override void OnUpdate()
var isConnected = false;
var isConnecting = false;
var isDisconnecting = false;
- if (TryGetSingletonEntity(out var singletonEntity))
+ if (SystemAPI.TryGetSingletonEntity(out var singletonEntity))
{
if (EntityManager.HasComponent(singletonEntity))
{
@@ -1234,7 +1233,7 @@ public void ToggleLagSpikeSimulator()
LagSpikeMillisecondsLeft = IsSimulatingLagSpike ? -1 : MultiplayerPlayModeWindow.k_LagSpikeDurationsSeconds[Prefs.LagSpikeSelectionIndex];
UpdateSimulator = true;
- GetSingletonRW().ValueRW.DebugLog($"Lag Spike Simulator: Toggled! Dropping packets for {Mathf.CeilToInt(LagSpikeMillisecondsLeft)}ms!");
+ SystemAPI.GetSingletonRW().ValueRW.DebugLog($"Lag Spike Simulator: Toggled! Dropping packets for {Mathf.CeilToInt(LagSpikeMillisecondsLeft)}ms!");
MultiplayerPlayModeWindow.ForceRepaint();
}
@@ -1244,7 +1243,7 @@ public void ToggleTimeoutSimulation()
ToggleLagSpikeSimulator();
var isSimulatingTimeout = IsSimulatingTimeout;
- GetSingletonRW().ValueRW.DebugLog($"Timeout Simulation: Toggled {(isSimulatingTimeout ? "OFF after {TimeoutSimulationDurationSeconds:0.0}s!" : "ON")}!");
+ SystemAPI.GetSingletonRW().ValueRW.DebugLog($"Timeout Simulation: Toggled {(isSimulatingTimeout ? "OFF after {TimeoutSimulationDurationSeconds:0.0}s!" : "ON")}!");
UpdateSimulator = true;
if (isSimulatingTimeout)
@@ -1275,7 +1274,7 @@ protected override void OnCreate()
}
protected override void OnUpdate()
{
- ref readonly var netStream = ref GetSingletonRW().ValueRO;
+ ref readonly var netStream = ref SystemAPI.GetSingletonRW().ValueRO;
IsListening = netStream.DriverStore.GetDriverInstance(netStream.DriverStore.FirstDriver).driver.Listening;
LastEndpoint = netStream.LastEndPoint;
}
@@ -1309,12 +1308,12 @@ protected override void OnUpdate()
if (World.IsThinClient())
return;
- if (TryGetSingleton(out var cfg))
+ if (SystemAPI.TryGetSingleton(out var cfg))
{
if (ShouldDumpPackets != IsDumpingPackets)
{
cfg.DumpPackets = ShouldDumpPackets;
- SetSingleton(cfg);
+ SystemAPI.SetSingleton(cfg);
}
else
ShouldDumpPackets = cfg.DumpPackets;
@@ -1352,4 +1351,48 @@ protected override void OnUpdate()
}
}
}
+
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.Editor)]
+ internal partial struct ConfigureClientGUIDSystem : ISystem
+ {
+ public void OnCreate(ref SystemState state)
+ {
+ bool canChangeSettings = (!UnityEditor.EditorApplication.isPlaying || state.WorldUnmanaged.IsClient());
+ if (canChangeSettings)
+ {
+ ref var sceneSystemGuid = ref state.EntityManager.GetComponentDataRW(state.World.GetExistingSystem()).ValueRW;
+ sceneSystemGuid.BuildConfigurationGUID = DotsGlobalSettings.Instance.GetClientGUID();
+ }
+ state.Enabled = false;
+ }
+
+ public void OnDestroy(ref SystemState state)
+ {}
+ public void OnUpdate(ref SystemState state)
+ {}
+ }
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ internal partial struct ConfigureServerGUIDSystem : ISystem
+ {
+ public void OnCreate(ref SystemState state)
+ {
+ ref var sceneSystemGuid = ref state.EntityManager.GetComponentDataRW(state.World.GetExistingSystem()).ValueRW;
+ // If client type is client-only server must use dedicated server data
+ if (((Unity.NetCode.Hybrid.ClientSettings)DotsGlobalSettings.Instance.ClientProvider).NetCodeClientTarget == NetCodeClientTarget.Client)
+ sceneSystemGuid.BuildConfigurationGUID = DotsGlobalSettings.Instance.GetServerGUID();
+ // If playmode is simulating dedicated server we must also use server data
+ else if (Prefs.SimulateDedicatedServer)
+ sceneSystemGuid.BuildConfigurationGUID = DotsGlobalSettings.Instance.GetServerGUID();
+ // Otherwise we use client & server data, we know the client is set to client & server at this point
+ else
+ sceneSystemGuid.BuildConfigurationGUID = DotsGlobalSettings.Instance.GetClientGUID();
+
+ state.Enabled = false;
+ }
+
+ public void OnDestroy(ref SystemState state)
+ {}
+ public void OnUpdate(ref SystemState state)
+ {}
+ }
}
diff --git a/Editor/Templates/CommandDataSerializer.cs b/Editor/Templates/CommandDataSerializer.cs
index cb0d284..36bb4a9 100644
--- a/Editor/Templates/CommandDataSerializer.cs
+++ b/Editor/Templates/CommandDataSerializer.cs
@@ -114,7 +114,7 @@ struct CompareJob : IJobChunk
[ReadOnly] public BufferTypeHandle<__COMMAND_COMPONENT_TYPE__> inputHandle;
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
{
- var inputBufferAccessor = chunk.GetBufferAccessor(inputHandle);
+ var inputBufferAccessor = chunk.GetBufferAccessor(ref inputHandle);
for (int entIdx = 0; entIdx < chunk.Count; ++entIdx)
{
var inputBuffer = inputBufferAccessor[entIdx];
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueBool.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueBool.cs
index 21b9e30..206dba1 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueBool.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueBool.cs
@@ -60,7 +60,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueDouble.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueDouble.cs
new file mode 100644
index 0000000..f2f88ac
--- /dev/null
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueDouble.cs
@@ -0,0 +1,104 @@
+#region __GHOST_IMPORTS__
+#endregion
+namespace Generated
+{
+ public struct GhostSnapshotData
+ {
+ struct Snapshot
+ {
+ #region __GHOST_FIELD__
+ public long __GHOST_FIELD_NAME__;
+ #endregion
+ }
+
+ public void PredictDelta(uint tick, ref GhostSnapshotData baseline1, ref GhostSnapshotData baseline2)
+ {
+ var predictor = new GhostDeltaPredictor(tick, this.tick, baseline1.tick, baseline2.tick);
+ #region __GHOST_PREDICT__
+ snapshot.__GHOST_FIELD_NAME__ = predictor.PredictLong(snapshot.__GHOST_FIELD_NAME__, baseline1.__GHOST_FIELD_NAME__, baseline2.__GHOST_FIELD_NAME__);
+ #endregion
+ }
+
+ public void Serialize(int networkId, ref GhostSnapshotData baseline, ref DataStreamWriter writer, StreamCompressionModel compressionModel)
+ {
+ #region __GHOST_WRITE__
+ if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0)
+ writer.WritePackedLongDelta(snapshot.__GHOST_FIELD_NAME__, baseline.__GHOST_FIELD_NAME__, compressionModel);
+ #endregion
+ }
+
+ public void Deserialize(uint tick, ref GhostSnapshotData baseline, ref DataStreamReader reader,
+ StreamCompressionModel compressionModel)
+ {
+ #region __GHOST_READ__
+ if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0)
+ snapshot.__GHOST_FIELD_NAME__ = reader.ReadPackedLongDelta(baseline.__GHOST_FIELD_NAME__, compressionModel);
+ else
+ snapshot.__GHOST_FIELD_NAME__ = baseline.__GHOST_FIELD_NAME__;
+ #endregion
+ }
+
+ public unsafe void CopyToSnapshot(ref Snapshot snapshot, ref IComponentData component)
+ {
+ if (true)
+ {
+ #region __GHOST_COPY_TO_SNAPSHOT__
+ snapshot.__GHOST_FIELD_NAME__ = (long) math.round(component.__GHOST_FIELD_REFERENCE__ * __GHOST_QUANTIZE_SCALE__);
+ #endregion
+ }
+ }
+ public unsafe void CopyFromSnapshot(ref Snapshot snapshot, ref IComponentData component)
+ {
+ if (true)
+ {
+ #region __GHOST_COPY_FROM_SNAPSHOT__
+ component.__GHOST_FIELD_REFERENCE__ = snapshotBefore.__GHOST_FIELD_NAME__ * (double)__GHOST_DEQUANTIZE_SCALE__;
+ #endregion
+
+ #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_SETUP__
+ var __GHOST_FIELD_NAME___Before = snapshotBefore.__GHOST_FIELD_NAME__ * (double)__GHOST_DEQUANTIZE_SCALE__;
+ var __GHOST_FIELD_NAME___After = snapshotAfter.__GHOST_FIELD_NAME__ * (double)__GHOST_DEQUANTIZE_SCALE__;
+ #endregion
+ #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_DISTSQ__
+ var __GHOST_FIELD_NAME___DistSq = math.distancesq(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After);
+ #endregion
+ #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE__
+ component.__GHOST_FIELD_REFERENCE__ = math.lerp(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After, snapshotInterpolationFactor);
+ #endregion
+ }
+ }
+ public unsafe void RestoreFromBackup(ref IComponentData component, in IComponentData backup)
+ {
+ #region __GHOST_RESTORE_FROM_BACKUP__
+ component.__GHOST_FIELD_REFERENCE__ = backup.__GHOST_FIELD_REFERENCE__;
+ #endregion
+ }
+ public void CalculateChangeMask(ref Snapshot snapshot, ref Snapshot baseline, uint changeMask)
+ {
+ #region __GHOST_CALCULATE_CHANGE_MASK_ZERO__
+ changeMask = (snapshot.__GHOST_FIELD_NAME__ != baseline.__GHOST_FIELD_NAME__) ? 1u : 0;
+ #endregion
+ #region __GHOST_CALCULATE_CHANGE_MASK__
+ changeMask |= (snapshot.__GHOST_FIELD_NAME__ != baseline.__GHOST_FIELD_NAME__) ? (1u<<__GHOST_MASK_INDEX__) : 0;
+ #endregion
+ }
+ #if UNITY_EDITOR || DEVELOPMENT_BUILD
+ private static void ReportPredictionErrors(ref IComponentData component, in IComponentData backup, ref UnsafeList errors, ref int errorIndex)
+ {
+ #region __GHOST_REPORT_PREDICTION_ERROR__
+ errors[errorIndex] = math.max(errors[errorIndex], (float)math.abs(component.__GHOST_FIELD_REFERENCE__ - backup.__GHOST_FIELD_REFERENCE__));
+ ++errorIndex;
+ #endregion
+ }
+ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref int nameCount)
+ {
+ #region __GHOST_GET_PREDICTION_ERROR_NAME__
+ if (nameCount != 0)
+ names.Append(new FixedString32Bytes(","));
+ names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ ++nameCount;
+ #endregion
+ }
+ #endif
+ }
+}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueDouble.cs.meta b/Editor/Templates/DefaultTypes/GhostSnapshotValueDouble.cs.meta
new file mode 100644
index 0000000..390089c
--- /dev/null
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueDouble.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 44428b1bedd14753b61ff4caee35fef9
+timeCreated: 1661788691
\ No newline at end of file
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueDoubleUnquantized.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueDoubleUnquantized.cs
new file mode 100644
index 0000000..43da301
--- /dev/null
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueDoubleUnquantized.cs
@@ -0,0 +1,102 @@
+#region __GHOST_IMPORTS__
+#endregion
+namespace Generated
+{
+ public struct GhostSnapshotData
+ {
+ struct Snapshot
+ {
+ #region __GHOST_FIELD__
+ public double __GHOST_FIELD_NAME__;
+ #endregion
+ }
+
+ public void PredictDelta(uint tick, ref GhostSnapshotData baseline1, ref GhostSnapshotData baseline2)
+ {
+ #region __GHOST_PREDICT__
+ #endregion
+ }
+
+ public void Serialize(int networkId, ref GhostSnapshotData baseline, ref DataStreamWriter writer, StreamCompressionModel compressionModel)
+ {
+ #region __GHOST_WRITE__
+ if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0)
+ writer.WritePackedDoubleDelta(snapshot.__GHOST_FIELD_NAME__, baseline.__GHOST_FIELD_NAME__, compressionModel);
+ #endregion
+ }
+
+ public void Deserialize(uint tick, ref GhostSnapshotData baseline, ref DataStreamReader reader,
+ StreamCompressionModel compressionModel)
+ {
+ #region __GHOST_READ__
+ if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0)
+ snapshot.__GHOST_FIELD_NAME__ = reader.ReadPackedDoubleDelta(baseline.__GHOST_FIELD_NAME__, compressionModel);
+ else
+ snapshot.__GHOST_FIELD_NAME__ = baseline.__GHOST_FIELD_NAME__;
+ #endregion
+ }
+
+ public unsafe void CopyToSnapshot(ref Snapshot snapshot, ref IComponentData component)
+ {
+ if (true)
+ {
+ #region __GHOST_COPY_TO_SNAPSHOT__
+ snapshot.__GHOST_FIELD_NAME__ = component.__GHOST_FIELD_REFERENCE__;
+ #endregion
+ }
+ }
+ public unsafe void CopyFromSnapshot(ref Snapshot snapshot, ref IComponentData component)
+ {
+ if (true)
+ {
+ #region __GHOST_COPY_FROM_SNAPSHOT__
+ component.__GHOST_FIELD_REFERENCE__ = snapshotBefore.__GHOST_FIELD_NAME__;
+ #endregion
+
+ #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_SETUP__
+ var __GHOST_FIELD_NAME___Before = snapshotBefore.__GHOST_FIELD_NAME__;
+ var __GHOST_FIELD_NAME___After = snapshotAfter.__GHOST_FIELD_NAME__;
+ #endregion
+ #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_DISTSQ__
+ var __GHOST_FIELD_NAME___DistSq = math.distancesq(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After);
+ #endregion
+ #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE__
+ component.__GHOST_FIELD_REFERENCE__ = math.lerp(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After, snapshotInterpolationFactor);
+ #endregion
+ }
+ }
+ public unsafe void RestoreFromBackup(ref IComponentData component, in IComponentData backup)
+ {
+ #region __GHOST_RESTORE_FROM_BACKUP__
+ component.__GHOST_FIELD_REFERENCE__ = backup.__GHOST_FIELD_REFERENCE__;
+ #endregion
+ }
+ public void CalculateChangeMask(ref Snapshot snapshot, ref Snapshot baseline, uint changeMask)
+ {
+ #region __GHOST_CALCULATE_CHANGE_MASK_ZERO__
+ changeMask = (snapshot.__GHOST_FIELD_NAME__ != baseline.__GHOST_FIELD_NAME__) ? 1u : 0;
+ #endregion
+ #region __GHOST_CALCULATE_CHANGE_MASK__
+ changeMask |= (snapshot.__GHOST_FIELD_NAME__ != baseline.__GHOST_FIELD_NAME__) ? (1u<<__GHOST_MASK_INDEX__) : 0;
+ #endregion
+ }
+ #if UNITY_EDITOR || DEVELOPMENT_BUILD
+ private static void ReportPredictionErrors(ref IComponentData component, in IComponentData backup, ref UnsafeList errors, ref int errorIndex)
+ {
+ #region __GHOST_REPORT_PREDICTION_ERROR__
+ errors[errorIndex] = math.max(errors[errorIndex], (float)math.abs(component.__GHOST_FIELD_REFERENCE__ - backup.__GHOST_FIELD_REFERENCE__));
+ ++errorIndex;
+ #endregion
+ }
+ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref int nameCount)
+ {
+ #region __GHOST_GET_PREDICTION_ERROR_NAME__
+ if (nameCount != 0)
+ names.Append(new FixedString32Bytes(","));
+ names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ ++nameCount;
+ #endregion
+ }
+ #endif
+ }
+}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueDoubleUnquantized.cs.meta b/Editor/Templates/DefaultTypes/GhostSnapshotValueDoubleUnquantized.cs.meta
new file mode 100644
index 0000000..6a7f3c0
--- /dev/null
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueDoubleUnquantized.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 627d57c9baff4509bfc3770e278f6077
+timeCreated: 1661788934
\ No newline at end of file
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat.cs
index b81e1e6..4cf1297 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat.cs
@@ -95,7 +95,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2.cs
index 3bbb7a8..5c77aa7 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2.cs
@@ -35,7 +35,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2Unquantized.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2Unquantized.cs
index fdf4efb..778f755 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2Unquantized.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat2Unquantized.cs
@@ -35,10 +35,10 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3.cs
index 16994e2..dade780 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3.cs
@@ -35,10 +35,10 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3Unquantized.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3Unquantized.cs
index 545582d..827d2c5 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3Unquantized.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat3Unquantized.cs
@@ -35,10 +35,10 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4.cs
index a21ab6b..3ecf9c3 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4.cs
@@ -35,10 +35,10 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4Unquantized.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4Unquantized.cs
index 9b96282..9dbf774 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4Unquantized.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloat4Unquantized.cs
@@ -35,10 +35,10 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloatUnquantized.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloatUnquantized.cs
index 6e5497d..1bcd46a 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueFloatUnquantized.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueFloatUnquantized.cs
@@ -110,7 +110,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueInt.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueInt.cs
index 8c66c10..b703ba1 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueInt.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueInt.cs
@@ -104,7 +104,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternion.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternion.cs
index 60c8917..6290d08 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternion.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternion.cs
@@ -125,7 +125,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternionUnquantized.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternionUnquantized.cs
index e3c94da..01fbe25 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternionUnquantized.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueQuaternionUnquantized.cs
@@ -158,7 +158,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/DefaultTypes/GhostSnapshotValueUInt.cs b/Editor/Templates/DefaultTypes/GhostSnapshotValueUInt.cs
index cbd36b7..7c9731f 100644
--- a/Editor/Templates/DefaultTypes/GhostSnapshotValueUInt.cs
+++ b/Editor/Templates/DefaultTypes/GhostSnapshotValueUInt.cs
@@ -105,7 +105,7 @@ private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref in
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0)
names.Append(new FixedString32Bytes(","));
- names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
+ names.Append((FixedString64Bytes)"__GHOST_FIELD_REFERENCE__");
++nameCount;
#endregion
}
diff --git a/Editor/Templates/GhostComponentMetaDataRegistrationSystem.cs b/Editor/Templates/GhostComponentMetaDataRegistrationSystem.cs
deleted file mode 100644
index bf8f033..0000000
--- a/Editor/Templates/GhostComponentMetaDataRegistrationSystem.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-//THIS FILE IS AUTOGENERATED BY GHOSTCOMPILER. DON'T MODIFY OR ALTER.
-using Unity.Burst;
-#region __GHOST_USING_STATEMENT__
-using __GHOST_USING__;
-#endregion
-
-namespace __GHOST_NAMESPACE__
-{
- [BurstCompile]
- [System.Runtime.CompilerServices.CompilerGenerated]
- [CreateAfter(typeof(GhostComponentSerializerCollectionSystemGroup))]
- [UpdateInGroup(typeof(GhostComponentSerializerCollectionSystemGroup))]
- public struct __REGISTRATION_SYSTEM_FILE_NAME__ : ISystem
- {
- [BurstCompile]
- public void OnCreate(ref Unity.Entities.SystemState state)
- {
- // Manual query as `SystemAPI.GetSingletonRW()` is throwing "fail to compile" errors.
- using var builder = new EntityQueryBuilder(Allocator.Temp).WithAllRW();
- using var query = state.EntityManager.CreateEntityQuery(builder);
- ref var data = ref query.GetSingletonRW().ValueRW;
-
- #region __GHOST_META_DATA_LIST__
- data.AddCodeGenTypeMetaData(new CodeGenTypeMetaData
- {
- TypeHash = __VARIANT_TYPE_HASH__,
- IsInputComponent = __TYPE_IS_INPUT_COMPONENT__,
- IsInputBuffer = __TYPE_IS_INPUT_BUFFER__,
- IsTestVariant = __TYPE_IS_TEST_VARIANT__,
- HasDontSupportPrefabOverridesAttribute = __TYPE_HAS_DONT_SUPPORT_PREFAB_OVERRIDES_ATTRIBUTE__,
- HasSupportsPrefabOverridesAttribute = __TYPE_HAS_SUPPORTS_PREFAB_OVERRIDES_ATTRIBUTE__,
- });
- #endregion
- }
-
- [BurstCompile]
- public void OnUpdate(ref Unity.Entities.SystemState state)
- {
- }
-
- [BurstCompile]
- public void OnDestroy(ref Unity.Entities.SystemState state)
- {
- }
- }
-}
diff --git a/Editor/Templates/GhostComponentMetaDataRegistrationSystem.cs.meta b/Editor/Templates/GhostComponentMetaDataRegistrationSystem.cs.meta
deleted file mode 100644
index 5e9f684..0000000
--- a/Editor/Templates/GhostComponentMetaDataRegistrationSystem.cs.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-fileFormatVersion: 2
-guid: db363c54abfe4eefa2d331ecb4b2a3d6
-timeCreated: 1657722955
\ No newline at end of file
diff --git a/Editor/Templates/GhostComponentSerializer.cs b/Editor/Templates/GhostComponentSerializer.cs
index 1e7fb48..efcb7f7 100644
--- a/Editor/Templates/GhostComponentSerializer.cs
+++ b/Editor/Templates/GhostComponentSerializer.cs
@@ -2,6 +2,10 @@
#region __GHOST_COMPONENT_IS_BUFFER__
#define COMPONENT_IS_BUFFER
#endregion
+#region __GHOST_COMPONENT_HAS_FIELDS__
+#define COMPONENT_HAS_GHOST_FIELDS
+#endregion
+
using System;
using System.Diagnostics;
using AOT;
@@ -24,23 +28,23 @@ internal struct __GHOST_NAME__GhostComponentSerializer
{
static GhostComponentSerializer.State GetState()
{
- // This needs to be lazy initialized because otherwise there is a depenency on the static initialization order which breaks il2cpp builds due to TYpeManager not being initialized yet
+ // This needs to be lazy initialized because otherwise there is a depenency on the static initialization order which breaks il2cpp builds, due to TypeManager not being initialized yet.
+ // Also, Burst function pointer compilation can take a while.
if (!s_StateInitialized)
{
s_State = new GhostComponentSerializer.State
{
GhostFieldsHash = __GHOST_FIELD_HASH__,
ComponentType = ComponentType.ReadWrite<__GHOST_COMPONENT_TYPE__>(),
- VariantTypeIndex = GhostComponentSerializer.VariantTypes.Count,
ComponentSize = UnsafeUtility.SizeOf<__GHOST_COMPONENT_TYPE__>(),
SnapshotSize = UnsafeUtility.SizeOf(),
ChangeMaskBits = ChangeMaskBits,
PrefabType = __GHOST_PREFAB_TYPE__,
SendMask = __GHOST_SEND_MASK__,
SendToOwner = __GHOST_SEND_OWNER__,
- SendForChildEntities = __GHOST_SEND_CHILD_ENTITY__,
VariantHash = __GHOST_VARIANT_HASH__,
- IsDefaultSerializer = __GHOST_IS_DEFAULT_SERIALIZER__,
+ SerializationStrategyIndex = -1,
+ SerializesEnabledBit = __GHOST_SERIALIZES_ENABLED_BIT__,
#if COMPONENT_IS_BUFFER
PostSerializeBuffer =
new PortableFunctionPointer(PostSerializeBuffer),
@@ -67,7 +71,15 @@ static GhostComponentSerializer.State GetState()
ProfilerMarker = new Unity.Profiling.ProfilerMarker("__GHOST_COMPONENT_TYPE__")
#endif
};
- GhostComponentSerializer.VariantTypes.Add(typeof(__GHOST_VARIANT_TYPE__));
+ // UnsafeUtility.SizeOf reports 1 with zero-sized components.
+ if (s_State.ComponentType.IsZeroSized)
+ {
+ s_State.ComponentSize = 0;
+ }
+#region __GHOST_HAS_NO_GHOST_FIELDS__
+ s_State.SnapshotSize = 0;
+#endregion
+
#if UNITY_EDITOR || DEVELOPMENT_BUILD || NETCODE_DEBUG
s_State.NumPredictionErrors = GetPredictionErrorNames(ref s_State.PredictionErrorNames);
#endif
@@ -104,6 +116,7 @@ private static void CheckDynamicMaskOffset(int offset, int sizeInBytes)
IntPtr snapshotDynamicDataPtr, IntPtr dynamicSizePerEntity, int dynamicSnapshotMaxOffset,
int len, ref int dynamicSnapshotDataOffset, int dynamicDataSize, int maskSize)
{
+ #if COMPONENT_HAS_GHOST_FIELDS
int PtrSize = UnsafeUtility.SizeOf();
const int IntSize = 4;
const int BaselinesPerEntity = 4;
@@ -194,7 +207,7 @@ private static void CheckDynamicMaskOffset(int offset, int sizeInBytes)
{
if (baselineDynamicDataPtr != IntPtr.Zero)
baselineData = GhostComponentSerializer.TypeCast(baselineDynamicDataPtr, maskSize + bOffset);
- Serialize(GhostComponentSerializer.TypeCast(snapshotDynamicDataPtr, maskSize + offset),
+ SerializeSnapshot(GhostComponentSerializer.TypeCast(snapshotDynamicDataPtr, maskSize + offset),
baselineData,
ref writer,
ref compressionModel,
@@ -213,11 +226,13 @@ private static void CheckDynamicMaskOffset(int offset, int sizeInBytes)
var missing = 32-writer.LengthInBits&31;
if (missing < 32)
writer.WriteRawBits(0, missing);
+ #endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.PostSerializeBufferDelegate))]
public static void PostSerializeBuffer(IntPtr snapshotData, int snapshotOffset, int snapshotStride, int maskOffsetInBits, int count, IntPtr baselines, ref DataStreamWriter writer, ref StreamCompressionModel compressionModel, IntPtr entityStartBit, IntPtr snapshotDynamicDataPtr, IntPtr dynamicSizePerEntity, int dynamicSnapshotMaxOffset)
{
+ #if COMPONENT_HAS_GHOST_FIELDS
int dynamicDataSize = UnsafeUtility.SizeOf();
for (int i = 0; i < count; ++i)
{
@@ -228,6 +243,7 @@ public static void PostSerializeBuffer(IntPtr snapshotData, int snapshotOffset,
CheckDynamicDataRange(dynamicSnapshotDataOffset, maskSize, len, dynamicDataSize, dynamicSnapshotMaxOffset);
SerializeOneBuffer(i, snapshotData, snapshotOffset, snapshotStride, maskOffsetInBits, baselines, ref writer, ref compressionModel, entityStartBit, snapshotDynamicDataPtr, dynamicSizePerEntity, dynamicSnapshotMaxOffset, len, ref dynamicSnapshotDataOffset, dynamicDataSize, maskSize);
}
+ #endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.SerializeBufferDelegate))]
@@ -238,6 +254,7 @@ public static void PostSerializeBuffer(IntPtr snapshotData, int snapshotOffset,
IntPtr entityStartBit, IntPtr snapshotDynamicDataPtr, ref int dynamicSnapshotDataOffset,
IntPtr dynamicSizePerEntity, int dynamicSnapshotMaxOffset)
{
+ #if COMPONENT_HAS_GHOST_FIELDS
int dynamicDataSize = UnsafeUtility.SizeOf();
for (int i = 0; i < count; ++i)
{
@@ -257,12 +274,14 @@ public static void PostSerializeBuffer(IntPtr snapshotData, int snapshotOffset,
}
SerializeOneBuffer(i, snapshotData, snapshotOffset, snapshotStride, maskOffsetInBits, baselines, ref writer, ref compressionModel, entityStartBit, snapshotDynamicDataPtr, dynamicSizePerEntity, dynamicSnapshotMaxOffset, len, ref dynamicSnapshotDataOffset, dynamicDataSize, maskSize);
}
+ #endif
}
#else
private static void SerializeOneEntity(int ent,
IntPtr snapshotData, int snapshotOffset, int snapshotStride, int maskOffsetInBits, IntPtr baselines,
ref DataStreamWriter writer, ref StreamCompressionModel compressionModel, IntPtr entityStartBit)
{
+ #if COMPONENT_HAS_GHOST_FIELDS
int PtrSize = UnsafeUtility.SizeOf();
const int IntSize = 4;
const int BaselinesPerEntity = 4;
@@ -290,21 +309,42 @@ public static void PostSerializeBuffer(IntPtr snapshotData, int snapshotOffset,
ref Snapshot snapshot =ref GhostComponentSerializer.TypeCast(snapshotData, snapshotOffset + snapshotStride*ent);
CalculateChangeMask(ref snapshot, baseline, snapshotData+IntSize + snapshotStride*ent, maskOffsetInBits);
- Serialize(snapshot, baseline, ref writer, ref compressionModel, snapshotData+IntSize + snapshotStride*ent, maskOffsetInBits);
+ SerializeSnapshot(snapshot, baseline, ref writer, ref compressionModel, snapshotData+IntSize + snapshotStride*ent, maskOffsetInBits);
ref var sbit = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*ent+IntSize);
sbit = writer.LengthInBits - startuint*32;
var missing = 32-writer.LengthInBits&31;
if (missing < 32)
writer.WriteRawBits(0, missing);
+ #else
+
+ // TODO: Move this outside code-gen, as we really dont need to do this here!
+ const int IntSize = 4;
+ ref var startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*ent);
+ startuint = writer.Length/IntSize;
+ startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*ent+IntSize);
+ startuint = 0;
+ #endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.PostSerializeDelegate))]
public static void PostSerialize(IntPtr snapshotData, int snapshotOffset, int snapshotStride, int maskOffsetInBits, int count, IntPtr baselines, ref DataStreamWriter writer, ref StreamCompressionModel compressionModel, IntPtr entityStartBit)
{
+ #if COMPONENT_HAS_GHOST_FIELDS
for (int i = 0; i < count; ++i)
{
SerializeOneEntity(i, snapshotData, snapshotOffset, snapshotStride, maskOffsetInBits, baselines, ref writer, ref compressionModel, entityStartBit);
}
+ #else
+ // TODO: Move this outside code-gen, as we really dont need to do this here!
+ for (int i = 0; i < count; ++i)
+ {
+ const int IntSize = 4;
+ ref var startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*i);
+ startuint = writer.Length/IntSize;
+ startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*i+IntSize);
+ startuint = 0;
+ }
+ #endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.SerializeDelegate))]
@@ -313,6 +353,7 @@ public static void PostSerialize(IntPtr snapshotData, int snapshotOffset, int sn
IntPtr componentData, int componentStride, int count, IntPtr baselines,
ref DataStreamWriter writer, ref StreamCompressionModel compressionModel, IntPtr entityStartBit)
{
+ #if COMPONENT_HAS_GHOST_FIELDS
ref var serializerState = ref GhostComponentSerializer.TypeCast(stateData, 0);
for (int i = 0; i < count; ++i)
{
@@ -327,6 +368,17 @@ public static void PostSerialize(IntPtr snapshotData, int snapshotOffset, int sn
SerializeOneEntity(i, snapshotData, snapshotOffset, snapshotStride, maskOffsetInBits, baselines, ref writer, ref compressionModel, entityStartBit);
}
+ #else
+ // TODO: Move this outside code-gen, as we really dont need to do this here!
+ for (int i = 0; i < count; ++i)
+ {
+ const int IntSize = 4;
+ ref var startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*i);
+ startuint = writer.Length/IntSize;
+ startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*i+IntSize);
+ startuint = 0;
+ }
+ #endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.SerializeChildDelegate))]
@@ -335,6 +387,7 @@ public static void PostSerialize(IntPtr snapshotData, int snapshotOffset, int sn
IntPtr componentData, int count, IntPtr baselines,
ref DataStreamWriter writer, ref StreamCompressionModel compressionModel, IntPtr entityStartBit)
{
+ #if COMPONENT_HAS_GHOST_FIELDS
ref var serializerState = ref GhostComponentSerializer.TypeCast(stateData, 0);
for (int i = 0; i < count; ++i)
{
@@ -350,18 +403,32 @@ public static void PostSerialize(IntPtr snapshotData, int snapshotOffset, int sn
SerializeOneEntity(i, snapshotData, snapshotOffset, snapshotStride, maskOffsetInBits, baselines, ref writer, ref compressionModel, entityStartBit);
}
+ #else
+ // TODO: Move this outside code-gen, as we really dont need to do this here!
+ for (int i = 0; i < count; ++i)
+ {
+ const int IntSize = 4;
+ ref var startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*i);
+ startuint = writer.Length/IntSize;
+ startuint = ref GhostComponentSerializer.TypeCast(entityStartBit, IntSize*2*i+IntSize);
+ startuint = 0;
+ }
+ #endif
}
#endif
private static void CopyToSnapshot(in GhostSerializerState serializerState, ref Snapshot snapshot, in __GHOST_COMPONENT_TYPE__ component)
{
+#if COMPONENT_HAS_GHOST_FIELDS
#region __GHOST_COPY_TO_SNAPSHOT__
#endregion
+#endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.CopyToFromSnapshotDelegate))]
public static void CopyToSnapshot(IntPtr stateData, IntPtr snapshotData, int snapshotOffset, int snapshotStride, IntPtr componentData, int componentStride, int count)
{
+#if COMPONENT_HAS_GHOST_FIELDS
for (int i = 0; i < count; ++i)
{
ref var snapshot = ref GhostComponentSerializer.TypeCast(snapshotData, snapshotOffset + snapshotStride*i);
@@ -369,11 +436,13 @@ public static void CopyToSnapshot(IntPtr stateData, IntPtr snapshotData, int sna
ref var serializerState = ref GhostComponentSerializer.TypeCast(stateData, 0);
CopyToSnapshot(serializerState, ref snapshot, component);
}
+#endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.CopyToFromSnapshotDelegate))]
public static void CopyFromSnapshot(IntPtr stateData, IntPtr snapshotData, int snapshotOffset, int snapshotStride, IntPtr componentData, int componentStride, int count)
{
+#if COMPONENT_HAS_GHOST_FIELDS
for (int i = 0; i < count; ++i)
{
var deserializerState = GhostComponentSerializer.TypeCast(stateData, 0);
@@ -417,6 +486,7 @@ public static void CopyFromSnapshot(IntPtr stateData, IntPtr snapshotData, int s
snapshotInterpolationFactor = 0;
#endregion
}
+#endif
}
@@ -424,24 +494,29 @@ public static void CopyFromSnapshot(IntPtr stateData, IntPtr snapshotData, int s
[MonoPInvokeCallback(typeof(GhostComponentSerializer.RestoreFromBackupDelegate))]
public static void RestoreFromBackup(IntPtr componentData, IntPtr backupData)
{
+#if COMPONENT_HAS_GHOST_FIELDS
ref var component = ref GhostComponentSerializer.TypeCast<__GHOST_COMPONENT_TYPE__>(componentData, 0);
ref var backup = ref GhostComponentSerializer.TypeCast<__GHOST_COMPONENT_TYPE__>(backupData, 0);
#region __GHOST_RESTORE_FROM_BACKUP__
#endregion
+#endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.PredictDeltaDelegate))]
public static void PredictDelta(IntPtr snapshotData, IntPtr baseline1Data, IntPtr baseline2Data, ref GhostDeltaPredictor predictor)
{
+#if COMPONENT_HAS_GHOST_FIELDS
ref var snapshot = ref GhostComponentSerializer.TypeCast(snapshotData);
ref var baseline1 = ref GhostComponentSerializer.TypeCast(baseline1Data);
ref var baseline2 = ref GhostComponentSerializer.TypeCast(baseline2Data);
#region __GHOST_PREDICT__
#endregion
+#endif
}
private static void CalculateChangeMask(ref Snapshot snapshot, in Snapshot baseline, IntPtr bits, int startOffset)
{
+#if COMPONENT_HAS_GHOST_FIELDS
uint changeMask;
#region __GHOST_CALCULATE_CHANGE_MASK__
#endregion
@@ -452,31 +527,37 @@ private static void CalculateChangeMask(ref Snapshot snapshot, in Snapshot basel
#region __GHOST_FLUSH_FINAL_COMPONENT_CHANGE_MASK__
GhostComponentSerializer.CopyToChangeMask(bits, changeMask, startOffset, __GHOST_CHANGE_MASK_BITS__);
#endregion
+#endif
}
- private static void Serialize(in Snapshot snapshot, in Snapshot baseline, ref DataStreamWriter writer, ref StreamCompressionModel compressionModel, IntPtr changeMaskData, int startOffset)
+ private static void SerializeSnapshot(in Snapshot snapshot, in Snapshot baseline, ref DataStreamWriter writer, ref StreamCompressionModel compressionModel, IntPtr changeMaskData, int startOffset)
{
+#if COMPONENT_HAS_GHOST_FIELDS
uint changeMask = GhostComponentSerializer.CopyFromChangeMask(changeMaskData, startOffset, ChangeMaskBits);
#region __GHOST_WRITE__
#endregion
#region __GHOST_REFRESH_CHANGE_MASK__
changeMask = GhostComponentSerializer.CopyFromChangeMask(changeMaskData, startOffset + __GHOST_CHANGE_MASK_BITS__, ChangeMaskBits - __GHOST_CHANGE_MASK_BITS__);
#endregion
+#endif
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.DeserializeDelegate))]
public static void Deserialize(IntPtr snapshotData, IntPtr baselineData, ref DataStreamReader reader, ref StreamCompressionModel compressionModel, IntPtr changeMaskData, int startOffset)
{
+#if COMPONENT_HAS_GHOST_FIELDS
ref var snapshot = ref GhostComponentSerializer.TypeCast(snapshotData);
ref var baseline = ref GhostComponentSerializer.TypeCast(baselineData);
uint changeMask = GhostComponentSerializer.CopyFromChangeMask(changeMaskData, startOffset, ChangeMaskBits);
#region __GHOST_READ__
#endregion
+#endif
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(GhostComponentSerializer.ReportPredictionErrorsDelegate))]
public static void ReportPredictionErrors(IntPtr componentData, IntPtr backupData, IntPtr errorsList, int errorsCount)
{
+#if COMPONENT_HAS_GHOST_FIELDS
#region __GHOST_PREDICTION_ERROR_HEADER__
ref var component = ref GhostComponentSerializer.TypeCast<__GHOST_COMPONENT_TYPE__>(componentData, 0);
ref var backup = ref GhostComponentSerializer.TypeCast<__GHOST_COMPONENT_TYPE__>(backupData, 0);
@@ -485,6 +566,7 @@ public static void ReportPredictionErrors(IntPtr componentData, IntPtr backupDat
#endregion
#region __GHOST_REPORT_PREDICTION_ERROR__
#endregion
+#endif
}
#endif
#if UNITY_EDITOR || DEVELOPMENT_BUILD || NETCODE_DEBUG
diff --git a/Editor/Templates/GhostComponentSerializerRegistrationSystem.cs b/Editor/Templates/GhostComponentSerializerRegistrationSystem.cs
index 363e487..a661ac0 100644
--- a/Editor/Templates/GhostComponentSerializerRegistrationSystem.cs
+++ b/Editor/Templates/GhostComponentSerializerRegistrationSystem.cs
@@ -1,5 +1,9 @@
//THIS FILE IS AUTOGENERATED BY GHOSTCOMPILER. DON'T MODIFY OR ALTER.
+
+using System.Text;
using Unity.Entities;
+using Unity.Burst;
+using Unity.Collections;
using Unity.NetCode;
using Unity.NetCode.LowLevel.Unsafe;
#region __GHOST_USING_STATEMENT__
@@ -10,30 +14,60 @@
#endregion
namespace __GHOST_NAMESPACE__
{
+ [BurstCompile]
[System.Runtime.CompilerServices.CompilerGenerated]
[UpdateInGroup(typeof(GhostComponentSerializerCollectionSystemGroup))]
[CreateAfter(typeof(GhostComponentSerializerCollectionSystemGroup))]
- public class GhostComponentSerializerRegistrationSystem : GhostComponentSerializerRegistrationSystemBase
+ public struct GhostComponentSerializerRegistrationSystem : ISystem, IGhostComponentSerializerRegistration
{
- protected override void OnCreate()
+ /// TODO - Not currently burst compiled due to statics in GhostComponentSerializer.State.
+ ///
+ public void OnCreate(ref SystemState state)
{
- ref var data = ref GetSingletonRW().ValueRW;
- #region __GHOST_COMPONENT_LIST__
- data.AddSerializer(__GHOST_NAME__GhostComponentSerializer.State);
- #endregion
- #region __GHOST_EMPTY_VARIANT_LIST__
- data.AddEmptyVariant(new VariantType
+ // Manual query as `SystemAPI.GetSingletonRW()` is throwing "fail to compile" errors.
+ using var builder = new EntityQueryBuilder(Allocator.Temp).WithAllRW();
+ using var query = state.EntityManager.CreateEntityQuery(builder);
+ ref var data = ref query.GetSingletonRW().ValueRW;
+
+ ComponentTypeSerializationStrategy ss = default;
+ #region __GHOST_SERIALIZATION_STRATEGY_LIST__
+ ss = new ComponentTypeSerializationStrategy
{
+ DisplayName = "__GHOST_VARIANT_DISPLAY_NAME__",
Component = ComponentType.ReadWrite<__GHOST_COMPONENT_TYPE__>(),
Hash = __GHOST_VARIANT_HASH__,
+ SelfIndex = -1,
+ SerializerIndex = -1,
PrefabType = __GHOST_PREFAB_TYPE__,
- VariantTypeIndex = GhostComponentSerializer.VariantTypes.Count,
- });
- GhostComponentSerializer.VariantTypes.Add(typeof(__VARIANT_TYPE__));
+ SendTypeOptimization = __GHOST_SEND_MASK__,
+ SendForChildEntities = __GHOST_SEND_CHILD_ENTITY__,
+ IsDefaultSerializer = __GHOST_IS_DEFAULT_SERIALIZER__,
+ IsInputComponent = __TYPE_IS_INPUT_COMPONENT__,
+ IsInputBuffer = __TYPE_IS_INPUT_BUFFER__,
+ IsTestVariant = __TYPE_IS_TEST_VARIANT__,
+ HasDontSupportPrefabOverridesAttribute = __TYPE_HAS_DONT_SUPPORT_PREFAB_OVERRIDES_ATTRIBUTE__,
+ HasSupportsPrefabOverridesAttribute = __TYPE_HAS_SUPPORTS_PREFAB_OVERRIDES_ATTRIBUTE__,
+ };
+ data.AddSerializationStrategy(ref ss);
+ #endregion
+
+ #region __GHOST_COMPONENT_LIST__
+ data.AddSerializer(__GHOST_NAME__GhostComponentSerializer.State);
#endregion
}
- protected override void OnUpdate()
+ /// Ignore. Disables the system.
+ ///
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ state.Enabled = false;
+ }
+
+ /// Ignore. Does nothing.
+ ///
+ [BurstCompile]
+ public void OnDestroy(ref SystemState state)
{
}
}
diff --git a/Editor/Unity.NetCode.Editor.asmdef b/Editor/Unity.NetCode.Editor.asmdef
index 5416fbb..ed61d18 100644
--- a/Editor/Unity.NetCode.Editor.asmdef
+++ b/Editor/Unity.NetCode.Editor.asmdef
@@ -16,7 +16,8 @@
"Unity.Entities.Editor",
"Unity.Properties.UI",
"Unity.Properties",
- "Unity.Properties.UI.Editor"
+ "Unity.Properties.UI.Editor",
+ "Unity.Entities.Build"
],
"includePlatforms": [
"Editor"
diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs
index ed1b820..cec0320 100644
--- a/Runtime/AssemblyInfo.cs
+++ b/Runtime/AssemblyInfo.cs
@@ -3,6 +3,7 @@
[assembly: InternalsVisibleTo("Unity.NetCode.EditorTests")]
[assembly: InternalsVisibleTo("Unity.NetCode.TestsUtils")]
[assembly: InternalsVisibleTo("Unity.NetCode.Authoring.Hybrid")]
+[assembly: InternalsVisibleTo("Unity.NetCode.Physics")]
[assembly: InternalsVisibleTo("Unity.NetCode.BurstCompatibilityCodeGenTests")]
[assembly: InternalsVisibleTo("Tests.ScenarioTests")]
diff --git a/Runtime/Authoring/DefaultVariantSystemBase.cs b/Runtime/Authoring/DefaultVariantSystemBase.cs
index e5e5e39..efa129b 100644
--- a/Runtime/Authoring/DefaultVariantSystemBase.cs
+++ b/Runtime/Authoring/DefaultVariantSystemBase.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using Unity.Entities;
-using System.Reflection;
using Unity.Collections;
-using UnityEngine;
+#if ENABLE_UNITY_COLLECTIONS_CHECKS && !UNITY_DOTSRUNTIME
+using System.Reflection;
+#endif
namespace Unity.NetCode
{
@@ -14,23 +14,23 @@ namespace Unity.NetCode
/// () for certain type.
/// A concrete implementation must implement the method and add to the dictionary
/// the desired type-variant pairs.
- ///
- /// The system must (and will be) created in both runtime and conversion worlds. During conversion, in particular,
- /// the GhostComponentSerializerCollectionSystemGroup is used by the `GhostAuthoringBakingSystem` to configure the ghost
+ /// The system must (and will be) created in both runtime and baking worlds. During baking, in particular,
+ /// the is used by the `GhostAuthoringBakingSystem` to configure the ghost
/// prefabs meta-data with the defaults values.
- ///
/// The abstract base class already has the correct flags / update in world attributes set.
- /// It is not necessary for the concrete implementation to specify the flags, nor the `UpdateInWorld`.
- ///
- /// There is also no particular restriction in which group the system need run in, since all data needed by the
- /// runtime is created inside the `OnCreate` method. As a general rule, if you really need to add an UpdateInGroup
- /// attribute, please use only the SimulationSystemGroup as target.
- ///
- /// This ensures this system is also added to all conversion worlds.
+ /// It is not necessary for the concrete implementation to specify the flags, nor the .
+ /// CREATION FLOW
+ ///
+ /// All the default variant systems must be created after the (that is responsible
+ /// to create the the default ghost variant mapping singleton). The `DefaultVariantSystemBase` already has the the correct
+ /// set, and it is not necessary for the sub-class to add the explicitly add/set this creation order again.
+ ///
///
/// You may have multiple derived systems. They'll all be read from, and conflicts will output errors at bake time, and the latest values will be used.
- [WorldSystemFilter(WorldSystemFilterFlags.BakingSystem | WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation |
+ WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.BakingSystem)]
[CreateAfter(typeof(GhostComponentSerializerCollectionSystemGroup))]
+ [UpdateInGroup(typeof(DefaultVariantSystemGroup))]
public abstract partial class DefaultVariantSystemBase : SystemBase
{
/// When defining default variants for a type, you must denote whether or not this variant will be applied to both parents and children.
@@ -54,7 +54,7 @@ public abstract partial class DefaultVariantSystemBase : SystemBase
/// This rule will add the same variant to all entities with this component type (i.e. both parent and children a.k.a. regardless of hierarchy).
/// Note: It is not recommended to serialize child entities as it is relatively slow to serialize them!
///
- /// ///
+ ///
public static Rule ForAll(Type variantForBoth) => new Rule(variantForBoth, variantForBoth);
/// This rule will add one variant for parents, and another variant for children, by default.
@@ -62,7 +62,8 @@ public abstract partial class DefaultVariantSystemBase : SystemBase
///
///
///
- public static Rule Unique(Type variantForParents, Type variantForChildren) => new Rule(variantForParents, variantForChildren);
+ public static Rule Unique(Type variantForParents, Type variantForChildren) =>
+ new Rule(variantForParents, variantForChildren);
/// This rule will only add this variant to child entities with this component.
/// The parent entities with this component will use the default serializer.
@@ -84,28 +85,41 @@ private Rule(Type variantForParents, Type variantForChildren)
/// The Rule string representation. Print the parent and child variant types.
///
///
- public override string ToString() => $"Rule[parents: `{VariantForParents}`, children: `{VariantForChildren}`]";
+ public override string ToString() =>
+ $"Rule[parents: `{VariantForParents}`, children: `{VariantForChildren}`]";
///
/// Compare two rules ana check if their parent and child types are identical.
///
///
///
- public bool Equals(Rule other) => VariantForParents == other.VariantForParents && VariantForChildren == other.VariantForChildren;
+ public bool Equals(Rule other) => VariantForParents == other.VariantForParents &&
+ VariantForChildren == other.VariantForChildren;
+ /// Unique HashCode if Variant fields are set.
+ ///
public override int GetHashCode()
{
unchecked
{
- return ((VariantForParents != null ? VariantForParents.GetHashCode() : 0) * 397) ^ (VariantForChildren != null ? VariantForChildren.GetHashCode() : 0);
+ return ((VariantForParents != null ? VariantForParents.GetHashCode() : 0) * 397) ^
+ (VariantForChildren != null ? VariantForChildren.GetHashCode() : 0);
}
}
- internal HashRule CreateHashRule(ComponentType componentType) => new HashRule(TryGetHashElseZero(componentType, VariantForParents), TryGetHashElseZero(componentType, VariantForChildren));
+ internal HashRule CreateHashRule(ComponentType componentType) => new HashRule(
+ TryGetHashElseZero(componentType, VariantForParents),
+ TryGetHashElseZero(componentType, VariantForChildren));
static ulong TryGetHashElseZero(ComponentType componentType, Type variantType)
{
- return variantType == null ? 0 : GhostVariantsUtility.UncheckedVariantHash(variantType.FullName, new FixedString512Bytes(componentType.GetDebugTypeName()));
+ if (variantType == null)
+ return 0;
+ if (variantType == typeof(DontSerializeVariant))
+ return GhostVariantsUtility.DontSerializeHash;
+ if (variantType == typeof(ClientOnlyVariant))
+ return GhostVariantsUtility.ClientOnlyHash;
+ return GhostVariantsUtility.UncheckedVariantHash(variantType.FullName, new FixedString512Bytes(componentType.GetDebugTypeName()));
}
}
@@ -114,6 +128,7 @@ static ulong TryGetHashElseZero(ComponentType componentType, Type variantType)
{
/// Hash version of .
public readonly ulong VariantForParents;
+
/// Hash version of .
public readonly ulong VariantForChildren;
@@ -123,9 +138,11 @@ public HashRule(ulong variantForParents, ulong variantForChildren)
VariantForChildren = variantForChildren;
}
- public override string ToString() => $"HashRule[parent: `{VariantForParents}`, children: `{VariantForChildren}`]";
+ public override string ToString() =>
+ $"HashRule[parent: `{VariantForParents}`, children: `{VariantForChildren}`]";
- public bool Equals(HashRule other) => VariantForParents == other.VariantForParents && VariantForChildren == other.VariantForChildren;
+ public bool Equals(HashRule other) => VariantForParents == other.VariantForParents &&
+ VariantForChildren == other.VariantForChildren;
}
@@ -135,33 +152,120 @@ protected sealed override void OnCreate()
//Some sanity check here are necessary
var defaultVariants = new Dictionary();
RegisterDefaultVariants(defaultVariants);
-#if ENABLE_UNITY_COLLECTIONS_CHECKS && !UNITY_DOTSRUNTIME
- ValidateUserSpecifiedDefaultVariants(defaultVariants);
+ var variantRules = World.GetExistingSystemManaged()
+ .DefaultVariantRules;
+ foreach (var rule in defaultVariants)
+ variantRules.SetDefaultVariant(rule.Key, rule.Value, this);
+ Enabled = false;
+ }
+
+ protected sealed override void OnUpdate()
+ {
+ }
+
+ ///
+ /// Implement this method by adding to the mapping your
+ /// default type->variant
+ ///
+ ///
+ protected abstract void RegisterDefaultVariants(Dictionary defaultVariants);
+ }
+
+ ///
+ /// Store the default component type -> ghost variant mapping (see ).
+ /// Used by systems implementing the abstract .
+ ///
+ internal class GhostVariantRules
+ {
+ public struct RuleAssignment
+ {
+ public DefaultVariantSystemBase.Rule Rule;
+ public SystemBase LastSystem;
+ }
+ private NativeHashMap DefaultVariants;
+
+#if ENABLE_UNITY_COLLECTIONS_CHECKS || NETCODE_DEBUG
+ //Used for debug purpose, track the latest assigned rule by each system. That help tracking
+ //down who is overwriting the the default rule, in case multiple systems responsible for assigning the default variants exists
+ //in the project.
+ private readonly Dictionary DefaultVariantsManaged;
#endif
- World.GetOrCreateSystemManaged().AppendUserSpecifiedDefaultVariantsToSystem(defaultVariants);
- Enabled = false;
+ public GhostVariantRules(NativeHashMap defaultVariants)
+ {
+ DefaultVariants = defaultVariants;
+#if ENABLE_UNITY_COLLECTIONS_CHECKS || NETCODE_DEBUG
+ DefaultVariantsManaged = new Dictionary(32);
+#endif
+ }
+
+ ///
+ /// Set the current variant to use by default
+ /// for the given component type.
+ /// If an entry for the component is already preent, the new will overwrite the current
+ /// assignment
+ ///
+ /// The component type for which you want to specify the variant to use.
+ /// The rule to assign.
+ /// The system that want to assign the rule. Used almost for debugging purpose
+ ///
+ public bool TrySetDefaultVariant(ComponentType componentType, DefaultVariantSystemBase.Rule rule, SystemBase currentSystem)
+ {
+#if ENABLE_UNITY_COLLECTIONS_CHECKS && !UNITY_DOTSRUNTIME
+ ValidateVariantRule(componentType, rule, currentSystem);
+#endif
+ var added = DefaultVariants.TryAdd(componentType, rule.CreateHashRule(componentType));
+#if ENABLE_UNITY_COLLECTIONS_CHECKS || NETCODE_DEBUG
+ if (added)
+ DefaultVariantsManaged[componentType] = new RuleAssignment { Rule = rule, LastSystem = currentSystem };
+#endif
+ return added;
}
- void ValidateUserSpecifiedDefaultVariants(Dictionary defaultVariants)
+ ///
+ /// Will set the current variant to use by default
+ /// for the given component type if a rule for the is not already present.
+ ///
+ /// The component type for which you want to specify the variant to use.
+ /// The rule to assign.
+ /// The system that want to assign the rule. Used almost for debugging purpose
+ ///
+ public void SetDefaultVariant(ComponentType componentType, DefaultVariantSystemBase.Rule rule, SystemBase currentSystem)
{
- foreach (var kvp in defaultVariants)
+#if ENABLE_UNITY_COLLECTIONS_CHECKS && !UNITY_DOTSRUNTIME
+ ValidateVariantRule(componentType, rule, currentSystem);
+#endif
+ var newRuleHash = rule.CreateHashRule(componentType);
+#if ENABLE_UNITY_COLLECTIONS_CHECKS || NETCODE_DEBUG
+ if (DefaultVariantsManaged.TryGetValue(componentType, out var existingRule))
{
- var componentType = kvp.Key;
- var rule = kvp.Value;
- if (rule.VariantForParents == default && rule.VariantForChildren == default)
- throw new System.ArgumentException($"`{componentType}` has an invalid default variant rule ({rule}) defined in `{GetType().FullName}` (in '{World.Name}'), as both are `null`!");
+ var rulesAreTheSame = existingRule.Rule.Equals(rule);
+ if (!rulesAreTheSame)
+ {
+ UnityEngine.Debug.Log($"`Overriding the default variant rule for type `{componentType.ToFixedString()}` with '{rule}' ('{newRuleHash}'). Previous rule was " +
+ $"('{existingRule.Rule}' ('{existingRule.Rule.CreateHashRule(componentType)}'), setup by {TypeManager.GetSystemName(existingRule.LastSystem.GetType())}.");
+ }
+ }
+ DefaultVariantsManaged[componentType] = new RuleAssignment{Rule = rule, LastSystem = currentSystem};
+#endif
+ DefaultVariants[componentType] = newRuleHash;
+ }
- var managedType = componentType.GetManagedType();
- if (typeof(IInputBufferData).IsAssignableFrom(managedType))
- throw new System.ArgumentException($"`{managedType}` is of type `IInputBufferData`, which must get its default variants from the `IInputComponentData` that it is code-generated from. Replace this dictionary entry ({rule}) with the `IInputComponentData` type in system `{GetType().FullName}`, in '{World.Name}'!");
+#if ENABLE_UNITY_COLLECTIONS_CHECKS && !UNITY_DOTSRUNTIME
+ void ValidateVariantRule(ComponentType componentType, DefaultVariantSystemBase.Rule rule, ComponentSystemBase systemBase)
+ {
+ if (rule.VariantForParents == default && rule.VariantForChildren == default)
+ throw new System.ArgumentException($"`{componentType}` has an invalid default variant rule ({rule}) defined in `{TypeManager.GetSystemName(systemBase.GetType())}` (in '{systemBase.World.Name}'), as both are `null`!");
- ValidateUserDefinedDefaultVariantRule(componentType, rule.VariantForParents);
- ValidateUserDefinedDefaultVariantRule(componentType, rule.VariantForChildren);
- }
+ var managedType = componentType.GetManagedType();
+ if (typeof(IInputBufferData).IsAssignableFrom(managedType))
+ throw new System.ArgumentException($"`{managedType}` is of type `IInputBufferData`, which must get its default variants from the `IInputComponentData` that it is code-generated from. Replace this dictionary entry ({rule}) with the `IInputComponentData` type in system `{TypeManager.GetSystemName(systemBase.GetType())}`, in '{systemBase.World.Name}'!");
+
+ ValidateUserDefinedDefaultVariantRule(componentType, rule.VariantForParents, systemBase);
+ ValidateUserDefinedDefaultVariantRule(componentType, rule.VariantForChildren, systemBase);
}
- void ValidateUserDefinedDefaultVariantRule(ComponentType componentType, Type variantType)
+ void ValidateUserDefinedDefaultVariantRule(ComponentType componentType, Type variantType, ComponentSystemBase systemBase)
{
// Nothing to validate if the variant is the "default serializer".
if (variantType == default || variantType == componentType.GetManagedType())
@@ -177,22 +281,12 @@ void ValidateUserDefinedDefaultVariantRule(ComponentType componentType, Type var
var variantAttr = variantType.GetCustomAttribute();
if (variantAttr == null)
- throw new System.ArgumentException($"Invalid type registered as default variant. GhostComponentVariationAttribute not found for type `{variantType.FullName}`, cannot use it as the default variant for `{componentType}`! Defined in system `{GetType().FullName}`!");
+ throw new System.ArgumentException($"Invalid type registered as default variant. GhostComponentVariationAttribute not found for type `{variantType.FullName}`, cannot use it as the default variant for `{componentType}`! Defined in system `{TypeManager.GetSystemName(systemBase.GetType())}`!");
var managedType = componentType.GetManagedType();
if (variantAttr.ComponentType != managedType)
- throw new System.ArgumentException($"`{variantType.FullName}` is not a variation of component `{componentType}`, cannot use it as a default variant in system `{GetType().FullName}`!");
+ throw new System.ArgumentException($"`{variantType.FullName}` is not a variation of component `{componentType}`, cannot use it as a default variant in system `{TypeManager.GetSystemName(systemBase.GetType())}`!");
}
-
- protected sealed override void OnUpdate()
- {
- }
-
- ///
- /// Implement this method by adding to the dictionary your
- /// default type->variant mapping.
- ///
- ///
- protected abstract void RegisterDefaultVariants(Dictionary defaultVariants);
+#endif
}
}
diff --git a/Runtime/Authoring/DefaultVariantSystemGroup.cs b/Runtime/Authoring/DefaultVariantSystemGroup.cs
new file mode 100644
index 0000000..7fa189e
--- /dev/null
+++ b/Runtime/Authoring/DefaultVariantSystemGroup.cs
@@ -0,0 +1,19 @@
+using Unity.Entities;
+
+namespace Unity.NetCode
+{
+ ///
+ /// Group that contains all the systems responsible to register/setup the default Ghost Variants (see ).
+ /// The system group OnCreate method finalize the default mapping inside its own `OnCreate` method, by collecting from all the registered
+ /// systems the set of variant to use.
+ /// The order in which variants are set in the map is governed by the update order (see , ).
+ ///
+ /// The group is present in both baking and client/server worlds.
+ ///
+ ///
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation |
+ WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.BakingSystem)]
+ public class DefaultVariantSystemGroup : ComponentSystemGroup
+ {
+ }
+}
diff --git a/Runtime/Authoring/DefaultVariantSystemGroup.cs.meta b/Runtime/Authoring/DefaultVariantSystemGroup.cs.meta
new file mode 100644
index 0000000..b75422c
--- /dev/null
+++ b/Runtime/Authoring/DefaultVariantSystemGroup.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f8c64f2287b549588243c55b6b1cc3ec
+timeCreated: 1667465755
\ No newline at end of file
diff --git a/Runtime/Authoring/GhostFieldAttribute.cs b/Runtime/Authoring/GhostFieldAttribute.cs
index b904902..5bba4d3 100644
--- a/Runtime/Authoring/GhostFieldAttribute.cs
+++ b/Runtime/Authoring/GhostFieldAttribute.cs
@@ -8,6 +8,8 @@ namespace Unity.NetCode
/// When a component or buffer contains at least one field that is annotated with a ,
/// a struct implementing the component serialization is automatically code-generated.
///
+ /// Note that "enableable components" () will still have their fields replicated, even when disabled.
+ /// See to replicate the enabled flag itself.
[AttributeUsage(AttributeTargets.Field|AttributeTargets.Property)]
public class GhostFieldAttribute : Attribute
{
@@ -65,7 +67,18 @@ public class GhostFieldAttribute : Attribute
}
///
- /// Add the attribute to prevent a field ICommandData struct to be serialized
+ /// Attribute denoting that an should have its enabled flag replicated.
+ /// And thus, this is only valid on enableable component types. You'll get compiler errors if it's not.
+ ///
+ /// A type will not replicate its enableable flag unless it has this attribute attached to the class.
+ /// This can (and should) also be added to variants that serialize enable bits.
+ [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)]
+ public sealed class GhostEnabledBitAttribute : Attribute
+ {
+ }
+
+ ///
+ /// Add the attribute to prevent a field ICommandData struct to be serialized.
///
[AttributeUsage(AttributeTargets.Field|AttributeTargets.Property, Inherited = true)]
public class DontSerializeForCommandAttribute : Attribute
diff --git a/Runtime/Authoring/Hybrid/AssemblyInfo.cs b/Runtime/Authoring/Hybrid/AssemblyInfo.cs
index 62af460..12b8ff4 100644
--- a/Runtime/Authoring/Hybrid/AssemblyInfo.cs
+++ b/Runtime/Authoring/Hybrid/AssemblyInfo.cs
@@ -3,5 +3,7 @@
[assembly: InternalsVisibleTo("Unity.NetCode.EditorTests")]
[assembly: InternalsVisibleTo("Unity.NetCode.TestsUtils")]
[assembly: InternalsVisibleTo("Unity.NetCode.Editor")]
+[assembly: InternalsVisibleTo("Unity.NetCode.Physics.Hybrid")]
+[assembly: InternalsVisibleTo("Unity.NetCode.Hybrid")]
[assembly: InternalsVisibleTo("Configuration")]
diff --git a/Runtime/Authoring/Hybrid/BakerExtension.cs b/Runtime/Authoring/Hybrid/BakerExtension.cs
index a9bbd46..e08f6ba 100644
--- a/Runtime/Authoring/Hybrid/BakerExtension.cs
+++ b/Runtime/Authoring/Hybrid/BakerExtension.cs
@@ -1,13 +1,16 @@
-#if UNITY_EDITOR
-using Authoring.Hybrid;
-using Unity.Entities.Conversion;
-#endif
using Unity.Entities;
-using UnityEditor;
using UnityEngine;
namespace Unity.NetCode.Hybrid
{
+ ///
+ /// Interface of the build settings that are used to build the client and server targets.
+ ///
+ internal interface INetCodeConversionTarget
+ {
+ NetcodeConversionTarget NetcodeTarget { get; }
+ }
+
///
/// A collection of extension utility methods for the used by NetCode during the baking process.
///
@@ -31,33 +34,18 @@ public static class BakerExtensions
{
// Detect target using build settings (This is used from sub scenes)
#if UNITY_EDITOR
+#if USING_PLATFORMS_PACKAGE
if (self.TryGetBuildConfigurationComponent(out var settings))
{
//Debug.LogWarning("BuildSettings conversion for: " + settings.Target);
return settings.Target;
}
+#endif
- if (self.IsBuiltInBuildsEnabled())
+ var settingAsset = self.GetDotsSettings();
+ if (settingAsset is INetCodeConversionTarget asset)
{
- var settingAsset = self.GetDotsSettings();
- if (settingAsset != null)
- {
- if (settingAsset is NetCodeClientSettings)
- {
- var asset = (NetCodeClientSettings) settingAsset;
- return asset.NetcodeTarget;
- }
- if (settingAsset is NetCodeClientAndServerSettings)
- {
- var asset = (NetCodeClientAndServerSettings) settingAsset;
- return asset.NetcodeTarget;
- }
- if (settingAsset is NetCodeServerSettings)
- {
- var asset = (NetCodeServerSettings) settingAsset;
- return asset.NetcodeTarget;
- }
- }
+ return asset.NetcodeTarget;
}
#endif
diff --git a/Runtime/Authoring/Hybrid/DefaultSmoothingActionUserParamsAuthoring.cs b/Runtime/Authoring/Hybrid/DefaultSmoothingActionUserParamsAuthoring.cs
index 0664d83..f63dee1 100644
--- a/Runtime/Authoring/Hybrid/DefaultSmoothingActionUserParamsAuthoring.cs
+++ b/Runtime/Authoring/Hybrid/DefaultSmoothingActionUserParamsAuthoring.cs
@@ -7,6 +7,7 @@ namespace Unity.NetCode
/// Authoring component which adds the maxDist component to the Entity.
///
[DisallowMultipleComponent]
+ [HelpURL(Authoring.HelpURLs.DefaultSmoothingActionUserParamsAuthoring)]
public class DefaultSmoothingActionUserParamsAuthoring : MonoBehaviour
{
[RegisterBinding(typeof(DefaultSmoothingActionUserParams), "maxDist")]
diff --git a/Runtime/Authoring/Hybrid/DisableAutomaticPrespawnSectionReportingAuthoring.cs b/Runtime/Authoring/Hybrid/DisableAutomaticPrespawnSectionReportingAuthoring.cs
index 1051666..59b7938 100644
--- a/Runtime/Authoring/Hybrid/DisableAutomaticPrespawnSectionReportingAuthoring.cs
+++ b/Runtime/Authoring/Hybrid/DisableAutomaticPrespawnSectionReportingAuthoring.cs
@@ -1,4 +1,5 @@
using Unity.Entities;
+using UnityEngine;
namespace Unity.NetCode
{
@@ -6,6 +7,7 @@ namespace Unity.NetCode
/// Authoring component which adds the DisableAutomaticPrespawnSectionReporting component to the Entity.
///
[UnityEngine.DisallowMultipleComponent]
+ [HelpURL(Authoring.HelpURLs.DisableAutomaticPrespawnSectionReportingAuthoring)]
public class DisableAutomaticPrespawnSectionReportingAuthoring : UnityEngine.MonoBehaviour
{
class DisableAutomaticPrespawnSectionReportingBaker : Baker
diff --git a/Runtime/Authoring/Hybrid/GhostAuthoringComponent.cs b/Runtime/Authoring/Hybrid/GhostAuthoringComponent.cs
index 7c87e49..a36830e 100644
--- a/Runtime/Authoring/Hybrid/GhostAuthoringComponent.cs
+++ b/Runtime/Authoring/Hybrid/GhostAuthoringComponent.cs
@@ -17,6 +17,7 @@ namespace Unity.NetCode
///
[RequireComponent(typeof(LinkedEntityGroupAuthoring))]
[DisallowMultipleComponent]
+ [HelpURL(Authoring.HelpURLs.GhostAuthoringComponent)]
public class GhostAuthoringComponent : MonoBehaviour
{
#if UNITY_EDITOR
@@ -51,18 +52,18 @@ void OnValidate()
///
/// The ghost modes supported by this ghost. This will perform some more optimizations at authoring time but make it impossible to change ghost mode at runtime.
///
- [Tooltip("The ghost modes supported by this ghost. This will perform some more optimizations at authoring time but make it impossible to change ghost mode at runtime.")]
+ [Tooltip("The ghost modes supported by this ghost. Setting to anything other than All will allow NetCode to perform some more optimizations at authoring time. However, it makes it impossible to change ghost mode at runtime.")]
public GhostModeMask SupportedGhostModes = GhostModeMask.All;
///
/// This setting is only for optimization, the ghost will be sent when modified regardless of this setting.
/// Optimizing for static makes snapshots slightly larger when they change, but smaller when they do not change.
///
- [Tooltip("This setting is only for optimization, the ghost will be sent when modified regardless of this setting. Optimizing for static makes snapshots slightly larger when they change, but smaller when they do not change.")]
+ [Tooltip("Optimization: Marking as `Static` makes snapshots slightly larger when GhostField values change, but smaller when they do not change.\n\nNote: This is just an optimization. I.e. Changes to GhostFields will always be replicated (it's just a question of how).")]
public GhostOptimizationMode OptimizationMode = GhostOptimizationMode.Dynamic;
///
/// If not all ghosts can fit in a snapshot only the most important ghosts will be sent. Higher importance means the ghost is more likely to be sent.
///
- [Tooltip("If not all ghosts can fit in a snapshot only the most important ghosts will be sent. Higher importance means the ghost is more likely to be sent.")]
+ [Tooltip("If not all ghosts can fit in a snapshot, only the most important ghosts will be sent. Higher importance means the ghost is more likely to be sent.")]
public int Importance = 1;
///
/// For internal use only, the prefab GUID used to distinguish between different variant of the same prefab.
@@ -72,13 +73,13 @@ void OnValidate()
/// Add a GhostOwnerComponent tracking which connection owns this component.
/// You must set the GhostOwnerComponent to a valid NetworkIdComponent.Value at runtime.
///
- [Tooltip("Add a GhostOwnerComponent tracking which connection owns this component. You must set the GhostOwnerComponent to a valid NetworkIdComponent.Value at runtime.")]
+ [Tooltip("Automatically adds a GhostOwnerComponent, which allows the server to set (and track) which connection owns this ghost. In your server code, you must set the GhostOwnerComponent to a valid NetworkIdComponent.Value at runtime.")]
public bool HasOwner;
///
/// Automatically send all ICommandData buffers if the ghost is owned by the current connection,
/// AutoCommandTarget.Enabled is true and the ghost is predicted.
///
- [Tooltip("Automatically send all ICommandData buffers if the ghost is owned by the current connection, AutoCommandTarget.Enabled is true and the ghost is predicted.")]
+ [Tooltip("Automatically sends all ICommandData buffers when the following conditions are met: \n\n - The ghost is owned by the current connection.\n\n - AutoCommandTarget is added, and Enabled is true.\n\n - The ghost is predicted.")]
public bool SupportAutoCommandTarget = true;
///
/// Add a CommandDataInterpolationDelay component so the interpolation delay of each client is tracked.
@@ -98,11 +99,13 @@ void OnValidate()
/// components on child entities or serialized buffers. A common case where this can be useful is the ghost
/// for the character / player.
///
- [Tooltip("Force this ghost to be quantized and copied to the snapshot format once for all connections instead of once per connection. This can save CPU time in the ghost send system if the ghost is almost always sent to at least one connection, and it contains many serialized components, serialized components on child entities or serialized buffers. A common case where this can be useful is the ghost for the character / player.")]
+ [Tooltip("Force this ghost to be quantized and copied to the snapshot format once for all connections instead of once per connection. This can save CPU time in the ghost send system if the ghost is almost always sent to at least one connection, and it contains many serialized components, serialized components on child entities, or serialized buffers. A common case where this can be useful is the ghost for the character / player.")]
public bool UsePreSerialization;
///
- /// The name of the GameObject prefab.
+ /// Validate the name of the GameObject prefab.
///
+ /// Outputs the hash generated from the name.
+ /// The FS equivalent of the gameObject.name.
public FixedString64Bytes GetAndValidateGhostName(out ulong ghostNameHash)
{
var ghostName = gameObject.name;
diff --git a/Runtime/Authoring/Hybrid/GhostAuthoringComponentBaker.cs b/Runtime/Authoring/Hybrid/GhostAuthoringComponentBaker.cs
index 80dad41..638e270 100644
--- a/Runtime/Authoring/Hybrid/GhostAuthoringComponentBaker.cs
+++ b/Runtime/Authoring/Hybrid/GhostAuthoringComponentBaker.cs
@@ -4,7 +4,6 @@
using Unity.Assertions;
using Unity.Collections;
using Unity.NetCode.Hybrid;
-using Unity.Transforms;
namespace Unity.NetCode
{
@@ -34,7 +33,10 @@ struct GhostAuthoringComponentBakingData : IComponentData
// Tracker for the predict spawn additional prefab created on clients so it can be cleaned up during baking process reset
[BakingType]
- struct AdditionalPrefab : IComponentData { }
+ struct AdditionalPrefab : IComponentData
+ {
+ public Entity RootEntity;
+ }
// This type is used to store the overrides
[BakingType]
@@ -131,12 +133,7 @@ public override void Bake(GhostAuthoringComponent ghostAuthoring)
};
// Generate a ghost type component so the ghost can be identified by mathcing prefab asset guid
- var ghostType = new GhostTypeComponent();
- ghostType.guid0 = Convert.ToUInt32(ghostAuthoring.prefabId.Substring(0, 8), 16);
- ghostType.guid1 = Convert.ToUInt32(ghostAuthoring.prefabId.Substring(8, 8), 16);
- ghostType.guid2 = Convert.ToUInt32(ghostAuthoring.prefabId.Substring(16, 8), 16);
- ghostType.guid3 = Convert.ToUInt32(ghostAuthoring.prefabId.Substring(24, 8), 16);
-
+ var ghostType = GhostTypeComponent.FromHash128String(ghostAuthoring.prefabId);
var activeInScene = IsActive();
AddComponent(new GhostAuthoringComponentBakingData
@@ -272,6 +269,17 @@ void RevertPreviousBakings(NativeParallelHashSet rootsToRebake)
EntityManager.RemoveComponent(childEntity, m_ChildRevertBakingComponents);
}
}).WithEntityQueryOptions(EntityQueryOptions.IncludePrefab).WithStructuralChanges().Run();
+
+ Entities
+ .ForEach((Entity childEntity, in AdditionalPrefab additionalPrefab) =>
+ {
+ if (rootsToRebake.Contains(additionalPrefab.RootEntity) ||
+ m_NoLongerBakedRootEntitiesMask.MatchesIgnoreFilter(additionalPrefab.RootEntity))
+ {
+ // Remove previously created additional client prefabs (as they'll be recreated)
+ EntityManager.DestroyEntity(childEntity);
+ }
+ }).WithEntityQueryOptions(EntityQueryOptions.IncludePrefab).WithStructuralChanges().Run();
}
void AddRevertBakingTags(NativeArray entities)
@@ -310,9 +318,16 @@ protected override void OnUpdate()
NativeParallelHashSet rootsToProcess = new NativeParallelHashSet(ghostCount, Allocator.TempJob);
var rootsToProcessWriter = rootsToProcess.AsParallelWriter();
var bakedMask = m_BakedEntityMask;
-
- // Remove previously created additional client prefabs (as they'll be recreated)
- EntityManager.DestroyEntity(m_AdditionalPrefabsQuery);
+
+ //ATTENTION! This singleton entity is always destroyed in the first non-incremental pass, because in the first import
+ //the baking system clean all the Entities in the world when you open a sub-scene.
+ //We recreate the entity here "lazily", so everything behave as expected.
+ if (!SystemAPI.TryGetSingleton(out var serializerCollectionData))
+ {
+ var systemGroup = World.GetExistingSystemManaged();
+ EntityManager.CreateSingleton(systemGroup.ghostComponentSerializerCollectionDataCache);
+ serializerCollectionData = systemGroup.ghostComponentSerializerCollectionDataCache;
+ }
// This code is selecting from all the roots, the ones that have been baked themselves or the ones where at least one child has been baked.
// The component BakedEntity is a TemporaryBakingType that is added to every entity that has baked on this baking pass.
@@ -336,7 +351,6 @@ protected override void OnUpdate()
// Revert the previously added components
RevertPreviousBakings(rootsToProcess);
- var collectionData = World.GetExistingSystemManaged().ghostComponentSerializerCollectionDataCache;
using (var context = new BlobAssetComputationContext(bakingSystem.BlobAssetStore, 16, Allocator.Temp))
{
Entities.ForEach((Entity rootEntity, DynamicBuffer linkedEntityGroup, in GhostAuthoringComponentBakingData ghostAuthoringBakingData) =>
@@ -386,14 +400,27 @@ protected override void OnUpdate()
//Initialize the value with common default and they overwrite them in case is necessary.
prefabTypes[compIdx] = GhostPrefabType.All;
- var variantType = collectionData.GetCurrentVariantTypeForComponentCached(allComponents[compIdx], myOverride.HasValue ? myOverride.Value.ComponentVariant : 0, !isChild);
+ var variantType = serializerCollectionData.GetCurrentSerializationStrategyForComponentCached(allComponents[compIdx], myOverride.HasValue ? myOverride.Value.ComponentVariant : 0, !isChild);
variants[compIdx] = variantType.Hash;
sendMasksOverride[compIdx] = GhostAuthoringInspectionComponent.ComponentOverride.NoOverride;
+ // NW: Disabled warning while investigating CI timeout error on mac: [TimeoutExceptionMessage]: Timeout while waiting for a log message, no editor logging has happened during the timeout window
+ //if (variantType.IsTestVariant != 0)
+ //{
+ // Debug.LogWarning($"Ghost '{ghostAuthoringBakingData.GhostName}' uses a test variant {variantType.ToFixedString()}! Ensure this is only ever used in an Editor, test context.");
+ //}
+
//Initialize the common default and then overwrite in case
if (myOverride.HasValue)
{
- variants[compIdx] = myOverride.Value.ComponentVariant;
+ if (myOverride.Value.ComponentVariant != 0) // Not an error if the hash is 0 (default).
+ {
+ if (variantType.Hash != myOverride.Value.ComponentVariant)
+ {
+ Debug.LogError($"Ghost '{ghostAuthoringBakingData.GhostName}' has an override for type {allComponents[compIdx].ToFixedString()} that sets the Variant to hash '{myOverride.Value.ComponentVariant}'. However, this hash is no longer present in code-gen, likely due to a code change removing or renaming the old variant. Thus, using Variant '{variantType.DisplayName}' (with hash: '{variantType.Hash}') and ignoring your \"Component Override\". Please open this prefab and re-apply.");
+ }
+ }
+
//Only override the the default if the property is meant to (so always check for UseDefaultValue first)
if (myOverride.Value.PrefabType != GhostAuthoringInspectionComponent.ComponentOverride.NoOverride)
prefabTypes[compIdx] = (GhostPrefabType) myOverride.Value.PrefabType;
@@ -484,24 +511,32 @@ protected override void OnUpdate()
(ghostAuthoringBakingData.BakingConfig.SupportedGhostModes & GhostModeMask.Predicted) == GhostModeMask.Predicted)
{
var additionalPrefab = EntityManager.Instantiate(rootEntity);
- EntityManager.AddComponent(additionalPrefab);
- // Update the serial with a high number to avoid an EntityGuid collision on the duplicated entity
- var guid = EntityManager.GetComponentData(additionalPrefab);
- var newGuid = new EntityGuid(guid.OriginatingId, 0, 0, (uint)(guid.b + 10000));
- EntityManager.SetComponentData(additionalPrefab, newGuid);
- EntityManager.AddComponent(additionalPrefab);
- var additionalLinkedEntities = GetBuffer(additionalPrefab);
+ EntityManager.AddComponentData(additionalPrefab, new AdditionalPrefab{RootEntity = rootEntity});
+ var additionalLinkedEntities = EntityManager.GetBuffer(additionalPrefab);
var childList = new NativeList(Allocator.Temp);
- for (int i = 1; i < additionalLinkedEntities.Length; ++i)
+ for (int i = 0; i < additionalLinkedEntities.Length; ++i)
{
var child = additionalLinkedEntities[i];
- guid = EntityManager.GetComponentData(child.Value);
- newGuid = new EntityGuid(guid.OriginatingId, 0, 0, (uint)(guid.b + 10000));
+ var guid = EntityManager.GetComponentData(child.Value);
+ var newGuid = new EntityGuid(guid.OriginatingId, 0, 0, (uint)(guid.b + 10000));
EntityManager.SetComponentData(child.Value, newGuid);
childList.Add(child.Value);
}
+ //We remove the TransformAuthoring component here because it is causing some issues with
+ //baking, since the new root entity (and all its additional ones) are not referenced by the baking system.
+ //In particular, when it comes to TransformAuthoringBakingSystem, because the TransformUsage is not
+ //present in the TransformUsages hashmap, the LocalTransform, LocalToWorld and other components are
+ //removed from the additional entity (some exception are triggered as well).
+
+ //Given how these special instantiated entity work and the fact it is reverted when necessary by this system.
+ //Another option is to remove the AdditionalEntityParent from all instantiated children to avoid involoutary
+ //changes.
+ //A better (an correct) solution would be to have a way to inform the baker about that new additional entity, that
+ //can be added earlier by a normal baker
+ EntityManager.RemoveComponent(childList.AsArray());
EntityManager.AddComponent(childList.AsArray());
EntityManager.AddComponent(rootEntity);
+
}
}
}).WithStructuralChanges().WithoutBurst().WithEntityQueryOptions(EntityQueryOptions.IncludePrefab).Run();
diff --git a/Runtime/Authoring/Hybrid/GhostAuthoringInspectionComponent.cs b/Runtime/Authoring/Hybrid/GhostAuthoringInspectionComponent.cs
index 1236668..f752a78 100644
--- a/Runtime/Authoring/Hybrid/GhostAuthoringInspectionComponent.cs
+++ b/Runtime/Authoring/Hybrid/GhostAuthoringInspectionComponent.cs
@@ -13,6 +13,7 @@ namespace Unity.NetCode
///
///
[DisallowMultipleComponent]
+ [HelpURL(Authoring.HelpURLs.GhostAuthoringInspetionComponent)]
public class GhostAuthoringInspectionComponent : MonoBehaviour
{
// TODO: This doesn't support multi-edit.
diff --git a/Runtime/Authoring/Hybrid/GhostPresentationGameObjectAuthoring.cs b/Runtime/Authoring/Hybrid/GhostPresentationGameObjectAuthoring.cs
index fd8550e..273417c 100644
--- a/Runtime/Authoring/Hybrid/GhostPresentationGameObjectAuthoring.cs
+++ b/Runtime/Authoring/Hybrid/GhostPresentationGameObjectAuthoring.cs
@@ -1,5 +1,7 @@
using Unity.Entities;
using UnityEngine;
+using System;
+using System.Collections.Generic;
namespace Unity.NetCode.Hybrid
{
@@ -12,6 +14,8 @@ namespace Unity.NetCode.Hybrid
/// It also add to the converted entity an that references the new created entity.
/// It finally register itself has a producer of IRegisterPlayableData.
///
+ [DisallowMultipleComponent]
+ [HelpURL(Authoring.HelpURLs.GhostPresentationGameObjectAuthoring)]
public class GhostPresentationGameObjectAuthoring : MonoBehaviour
#if !UNITY_DISABLE_MANAGED_COMPONENTS
, IRegisterPlayableData
@@ -48,6 +52,7 @@ class GhostPresentationGameObjectBaker : Baker m_AddedTypes;
public override void Bake(GhostPresentationGameObjectAuthoring authoring)
{
#if UNITY_DISABLE_MANAGED_COMPONENTS
@@ -64,12 +69,13 @@ public override void Bake(GhostPresentationGameObjectAuthoring authoring)
};
if (prefabComponent.Server == null && prefabComponent.Client == null)
return;
- var presPrefab = CreateAdditionalEntity();
+ var presPrefab = CreateAdditionalEntity(TransformUsageFlags.None);
AddComponentObject(presPrefab, prefabComponent);
AddComponent(new GhostPresentationGameObjectPrefabReference{Prefab = presPrefab});
// Register all the components needed for animation data
+ m_AddedTypes = new HashSet();
if (prefabComponent.Client != null)
{
var anim = GetComponent(prefabComponent.Client);
@@ -88,6 +94,9 @@ public override void Bake(GhostPresentationGameObjectAuthoring authoring)
#if !UNITY_DISABLE_MANAGED_COMPONENTS
public void RegisterPlayableData() where T: unmanaged, IComponentData
{
+ if (m_AddedTypes.Contains(typeof(T)))
+ return;
+ m_AddedTypes.Add(typeof(T));
AddComponent(default(T));
}
#endif
diff --git a/Runtime/Authoring/Hybrid/HelpURL.cs b/Runtime/Authoring/Hybrid/HelpURL.cs
new file mode 100644
index 0000000..5d2f6a3
--- /dev/null
+++ b/Runtime/Authoring/Hybrid/HelpURL.cs
@@ -0,0 +1,14 @@
+namespace Unity.NetCode.Authoring
+{
+ internal static partial class HelpURLs
+ {
+ const string k_BaseUrl = "https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/";
+ internal const string GhostAuthoringComponent = k_BaseUrl + "Unity.NetCode.GhostAuthoringComponent.html";
+ internal const string GhostAuthoringInspetionComponent = k_BaseUrl + "Unity.NetCode.GhostAuthoringInspectionComponent.html";
+ internal const string NetCodeDebugConfigAuthoring = k_BaseUrl + "Unity.NetCode.NetCodeDebugConfigAuthoring.html";
+ internal const string DefaultSmoothingActionUserParamsAuthoring = k_BaseUrl + "Unity.NetCode.DefaultSmoothingActionUserParamsAuthoring.html";
+ internal const string DisableAutomaticPrespawnSectionReportingAuthoring = k_BaseUrl + "Unity.NetCode.DisableAutomaticPrespawnSectionReportingAuthoring.html";
+ internal const string NetCodePhysicsConfig = k_BaseUrl + "Unity.NetCode.NetCodePhysicsConfig.html";
+ internal const string GhostPresentationGameObjectAuthoring = k_BaseUrl + "Unity.NetCode.Hybrid.GhostPresentationGameObjectAuthoring.html";
+ }
+}
diff --git a/Runtime/Authoring/Hybrid/HelpURL.cs.meta b/Runtime/Authoring/Hybrid/HelpURL.cs.meta
new file mode 100644
index 0000000..a0e4db3
--- /dev/null
+++ b/Runtime/Authoring/Hybrid/HelpURL.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a3d4d88678af477b9f9f2cf4989b96ae
+timeCreated: 1666232376
\ No newline at end of file
diff --git a/Runtime/Authoring/Hybrid/NetCodeClientAndServerSettings.cs b/Runtime/Authoring/Hybrid/NetCodeClientAndServerSettings.cs
index 10c9449..563f5ef 100644
--- a/Runtime/Authoring/Hybrid/NetCodeClientAndServerSettings.cs
+++ b/Runtime/Authoring/Hybrid/NetCodeClientAndServerSettings.cs
@@ -1,26 +1,75 @@
#if UNITY_EDITOR
using System;
using Unity.Entities.Build;
+using UnityEditor;
using UnityEngine;
-namespace Authoring.Hybrid
+namespace Unity.NetCode.Hybrid
{
- public class NetCodeClientAndServerSettings: DotsPlayerSettings
+ [FilePath("ProjectSettings/NetCodeClientAndServerSettings.asset", FilePathAttribute.Location.ProjectFolder)]
+ internal class NetCodeClientAndServerSettings : ScriptableSingleton, IEntitiesPlayerSettings, INetCodeConversionTarget
{
+ NetcodeConversionTarget INetCodeConversionTarget.NetcodeTarget => NetcodeConversionTarget.ClientAndServer;
+
[SerializeField]
- public NetcodeConversionTarget NetcodeTarget = NetcodeConversionTarget.ClientAndServer;
+ public BakingSystemFilterSettings FilterSettings;
+
[SerializeField]
public string[] AdditionalScriptingDefines = Array.Empty();
- public override BakingSystemFilterSettings GetFilterSettings()
+ static Entities.Hash128 s_Guid;
+ public Entities.Hash128 GUID
+ {
+ get
+ {
+ if (!s_Guid.IsValid)
+ s_Guid = UnityEngine.Hash128.Compute(GetFilePath());
+ return s_Guid;
+ }
+ }
+
+ public string CustomDependency => GetFilePath();
+ void IEntitiesPlayerSettings.RegisterCustomDependency()
+ {
+ var hash = GetHash();
+ AssetDatabase.RegisterCustomDependency(CustomDependency, hash);
+ }
+
+ public UnityEngine.Hash128 GetHash()
{
- return null;
+ var hash = (UnityEngine.Hash128)GUID;
+ if (FilterSettings?.ExcludedBakingSystemAssemblies != null)
+ foreach (var assembly in FilterSettings.ExcludedBakingSystemAssemblies)
+ {
+ var guid = AssetDatabase.GUIDFromAssetPath(AssetDatabase.GetAssetPath(assembly.asset));
+ hash.Append(ref guid);
+ }
+ foreach (var define in AdditionalScriptingDefines)
+ hash.Append(define);
+ return hash;
}
- public override string[] GetAdditionalScriptingDefines()
+ public BakingSystemFilterSettings GetFilterSettings()
+ {
+ return FilterSettings;
+ }
+
+ public string[] GetAdditionalScriptingDefines()
{
return AdditionalScriptingDefines;
}
+
+ ScriptableObject IEntitiesPlayerSettings.AsScriptableObject() => instance;
+
+ internal void Save()
+ {
+ Save(true);
+ ((IEntitiesPlayerSettings)this).RegisterCustomDependency();
+ if (!AssetDatabase.IsAssetImportWorkerProcess())
+ AssetDatabase.Refresh();
+ }
+
+ private void OnDisable() => Save();
}
}
#endif
diff --git a/Runtime/Authoring/Hybrid/NetCodeClientSettings.cs b/Runtime/Authoring/Hybrid/NetCodeClientSettings.cs
index 41838c8..f100e12 100644
--- a/Runtime/Authoring/Hybrid/NetCodeClientSettings.cs
+++ b/Runtime/Authoring/Hybrid/NetCodeClientSettings.cs
@@ -1,6 +1,5 @@
#if UNITY_EDITOR
using System;
-using System.IO;
using System.Linq;
using Unity.Entities.Build;
using UnityEditor;
@@ -9,18 +8,20 @@
using UnityEngine.UIElements;
using Hash128 = Unity.Entities.Hash128;
-namespace Authoring.Hybrid
+namespace Unity.NetCode.Hybrid
{
public enum NetCodeClientTarget
{
+ [Tooltip("Build a client-only player.")]
Client = 0,
+ [Tooltip("Build a client-server player.")]
ClientAndServer = 1
}
- internal class NetCodeClientSettings : DotsPlayerSettings
+ [FilePath("ProjectSettings/NetCodeClientSettings.asset", FilePathAttribute.Location.ProjectFolder)]
+ internal class NetCodeClientSettings : ScriptableSingleton, IEntitiesPlayerSettings, INetCodeConversionTarget
{
- [SerializeField]
- public NetcodeConversionTarget NetcodeTarget = NetcodeConversionTarget.Client;
+ NetcodeConversionTarget INetCodeConversionTarget.NetcodeTarget => NetcodeConversionTarget.Client;
[SerializeField]
public BakingSystemFilterSettings FilterSettings;
@@ -28,18 +29,60 @@ internal class NetCodeClientSettings : DotsPlayerSettings
[SerializeField]
public string[] AdditionalScriptingDefines = Array.Empty();
- public override BakingSystemFilterSettings GetFilterSettings()
+ static Entities.Hash128 s_Guid;
+ public Entities.Hash128 GUID
+ {
+ get
+ {
+ if (!s_Guid.IsValid)
+ s_Guid = UnityEngine.Hash128.Compute(GetFilePath());
+ return s_Guid;
+ }
+ }
+ public string CustomDependency => GetFilePath();
+ void IEntitiesPlayerSettings.RegisterCustomDependency()
+ {
+ var hash = GetHash();
+ AssetDatabase.RegisterCustomDependency(CustomDependency, hash);
+ }
+
+ public UnityEngine.Hash128 GetHash()
+ {
+ var hash = (UnityEngine.Hash128)GUID;
+ if (FilterSettings?.ExcludedBakingSystemAssemblies != null)
+ foreach (var assembly in FilterSettings.ExcludedBakingSystemAssemblies)
+ {
+ var guid = AssetDatabase.GUIDFromAssetPath(AssetDatabase.GetAssetPath(assembly.asset));
+ hash.Append(ref guid);
+ }
+ foreach (var define in AdditionalScriptingDefines)
+ hash.Append(define);
+ return hash;
+ }
+
+ public BakingSystemFilterSettings GetFilterSettings()
{
return FilterSettings;
}
- public override string[] GetAdditionalScriptingDefines()
+ public string[] GetAdditionalScriptingDefines()
{
return AdditionalScriptingDefines;
}
+
+ ScriptableObject IEntitiesPlayerSettings.AsScriptableObject() => instance;
+
+ internal void Save()
+ {
+ Save(true);
+ ((IEntitiesPlayerSettings)this).RegisterCustomDependency();
+ if (!AssetDatabase.IsAssetImportWorkerProcess())
+ AssetDatabase.Refresh();
+ }
+ private void OnDisable() { Save(); }
}
- public class ClientSettings : DotsPlayerSettingsProvider
+ internal class ClientSettings : DotsPlayerSettingsProvider
{
private const string m_EditorPrefsNetCodeClientTarget = "com.unity.entities.netcodeclient.target";
@@ -51,12 +94,6 @@ public NetCodeClientTarget NetCodeClientTarget
private VisualElement m_rootElement;
- private NetCodeClientSettings m_NetCodeClientSettings;
- private NetCodeClientAndServerSettings m_NetCodeClientAndServerSettings;
-
- private Hash128 m_ClientGUID;
- private Hash128 m_ClientAndServerGUID;
-
public override int Importance
{
get { return 1; }
@@ -67,7 +104,7 @@ public override DotsGlobalSettings.PlayerType GetPlayerType()
return DotsGlobalSettings.PlayerType.Client;
}
- public override Hash128 GetPlayerSettingGUID()
+ protected override Hash128 DoGetPlayerSettingGUID()
{
return GetSettingGUID(NetCodeClientTarget);
}
@@ -76,27 +113,21 @@ public Hash128 GetSettingGUID(NetCodeClientTarget target)
{
if (target == NetCodeClientTarget.Client)
{
- if(!m_ClientGUID.IsValid)
- LoadOrCreateClientAsset();
- return m_ClientGUID;
+ return NetCodeClientSettings.instance.GUID;
}
if (target == NetCodeClientTarget.ClientAndServer)
{
- if(!m_ClientAndServerGUID.IsValid)
- LoadOrCreateClientAndServerAsset();
- return m_ClientAndServerGUID;
+ return NetCodeClientAndServerSettings.instance.GUID;
}
- return new Hash128();
- }
-
- public override void Enable(int value)
- {
- m_rootElement.SetEnabled((value == (int)DotsGlobalSettings.PlayerType.Client));
+ return default;
}
public override void OnActivate(DotsGlobalSettings.PlayerType type, VisualElement rootElement)
{
+ rootElement.RegisterCallback(OnAttachToPanel);
+ rootElement.RegisterCallback(OnDetachFromPanel);
+
m_rootElement = new VisualElement();
m_rootElement.SetEnabled(type == DotsGlobalSettings.PlayerType.Client);
@@ -106,13 +137,29 @@ public override void OnActivate(DotsGlobalSettings.PlayerType type, VisualElemen
rootElement.Add(m_rootElement);
}
+ static void OnAttachToPanel(AttachToPanelEvent evt)
+ {
+ // The ScriptableSingleton is not directly editable by default.
+ // Change the hideFlags to make the SerializedObject editable.
+ NetCodeClientSettings.instance.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
+ NetCodeClientAndServerSettings.instance.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
+ }
+
+ static void OnDetachFromPanel(DetachFromPanelEvent evt)
+ {
+ NetCodeClientSettings.instance.hideFlags = HideFlags.HideAndDontSave;
+ NetCodeClientAndServerSettings.instance.hideFlags = HideFlags.HideAndDontSave;
+ NetCodeClientSettings.instance.Save();
+ NetCodeClientAndServerSettings.instance.Save();
+ }
+
VisualElement UpdateUI()
{
var targetElement = new VisualElement();
targetElement.name = "target";
targetElement.AddToClassList("target");
- var so = new SerializedObject(GetSettingAsset());
+ var so = new SerializedObject(GetSettingAsset().AsScriptableObject());
targetElement.Bind(so);
so.Update();
@@ -125,19 +172,7 @@ VisualElement UpdateUI()
var field = new EnumField("NetCode client target:", NetCodeClientTarget);
targetS.Add(field);
- if (NetCodeClientTarget == NetCodeClientTarget.Client)
- {
- var propClientSettings = so.FindProperty("FilterSettings");
- var propClientField = new PropertyField(propClientSettings.FindPropertyRelative("ExcludedBakingSystemAssemblies"));
- propClientField.name = "ClientFilterSettings";
- targetS.Add(propClientField);
- }
- else
- {
- var propClientField = targetS.Q("ClientFilterSettings");
- if (propClientField != null)
- targetS.Remove(propClientField);
- }
+ targetS.Add(new PropertyField(so.FindProperty("FilterSettings.ExcludedBakingSystemAssemblies")));
var propExtraDefines = so.FindProperty("AdditionalScriptingDefines");
var propExtraDefinesField = new PropertyField(propExtraDefines);
@@ -168,60 +203,19 @@ public override string[] GetExtraScriptingDefines()
return Array.Empty();
}
- public override DotsPlayerSettings GetSettingAsset()
+ protected override IEntitiesPlayerSettings DoGetSettingAsset()
{
if (NetCodeClientTarget == NetCodeClientTarget.Client)
{
- if (m_NetCodeClientSettings == null)
- LoadOrCreateClientAsset();
- return m_NetCodeClientSettings;
+ return NetCodeClientSettings.instance;
}
if (NetCodeClientTarget == NetCodeClientTarget.ClientAndServer)
{
- if(m_NetCodeClientAndServerSettings == null)
- LoadOrCreateClientAndServerAsset();
- return m_NetCodeClientAndServerSettings;
+ return NetCodeClientAndServerSettings.instance;
}
return null;
}
-
- void LoadOrCreateClientAsset()
- {
- var path = k_DefaultAssetPath + k_DefaultAssetName + "ClientSettings" + k_DefaultAssetExtension;
- if(File.Exists(path))
- m_NetCodeClientSettings = AssetDatabase.LoadAssetAtPath(path);
- else
- {
- //Create the Client asset
- m_NetCodeClientSettings = (NetCodeClientSettings)ScriptableObject.CreateInstance(typeof(NetCodeClientSettings));
- m_NetCodeClientSettings.NetcodeTarget = NetcodeConversionTarget.Client;
- m_NetCodeClientSettings.name = k_DefaultAssetName + nameof(NetCodeClientSettings);
-
- AssetDatabase.CreateAsset(m_NetCodeClientSettings, path);
- }
- m_ClientGUID = new Hash128(AssetDatabase.AssetPathToGUID(path));
- }
-
- void LoadOrCreateClientAndServerAsset()
- {
- if (m_NetCodeClientAndServerSettings == null)
- {
- var path = k_DefaultAssetPath + k_DefaultAssetName + "ClientAndServerSettings" + k_DefaultAssetExtension;
- if(File.Exists(path))
- m_NetCodeClientAndServerSettings = AssetDatabase.LoadAssetAtPath(path);
- else
- {
- //Create the ClientAndServer asset
- m_NetCodeClientAndServerSettings = (NetCodeClientAndServerSettings)ScriptableObject.CreateInstance(typeof(NetCodeClientAndServerSettings));
- m_NetCodeClientAndServerSettings.NetcodeTarget = NetcodeConversionTarget.ClientAndServer;
- m_NetCodeClientAndServerSettings.name = k_DefaultAssetName + nameof(NetCodeClientAndServerSettings);
-
- AssetDatabase.CreateAsset(m_NetCodeClientAndServerSettings, path);
- }
- m_ClientAndServerGUID = new Hash128(AssetDatabase.AssetPathToGUID(path));
- }
- }
}
}
#endif
diff --git a/Runtime/Authoring/Hybrid/NetCodeConversionSettings.cs b/Runtime/Authoring/Hybrid/NetCodeConversionSettings.cs
index 9514cc4..a8ffbd3 100644
--- a/Runtime/Authoring/Hybrid/NetCodeConversionSettings.cs
+++ b/Runtime/Authoring/Hybrid/NetCodeConversionSettings.cs
@@ -1,3 +1,4 @@
+#if USING_PLATFORMS_PACKAGE
#if UNITY_EDITOR
using Unity.Build;
#endif
@@ -16,3 +17,4 @@ public bool OnGUI()
}
}
#endif
+#endif
diff --git a/Runtime/Authoring/Hybrid/NetCodeDebugConfigAuthoring.cs b/Runtime/Authoring/Hybrid/NetCodeDebugConfigAuthoring.cs
index b0ec860..fafb38a 100644
--- a/Runtime/Authoring/Hybrid/NetCodeDebugConfigAuthoring.cs
+++ b/Runtime/Authoring/Hybrid/NetCodeDebugConfigAuthoring.cs
@@ -7,6 +7,7 @@ namespace Unity.NetCode
/// Add this component to a gameobject present in a sub-scene to configure the logging level and
/// enable packet dumps.
///
+ [HelpURL(Authoring.HelpURLs.NetCodeDebugConfigAuthoring)]
public class NetCodeDebugConfigAuthoring : MonoBehaviour
{
///
diff --git a/Runtime/Authoring/Hybrid/NetCodeServerSettings.cs b/Runtime/Authoring/Hybrid/NetCodeServerSettings.cs
index b1ec394..8b5ed81 100644
--- a/Runtime/Authoring/Hybrid/NetCodeServerSettings.cs
+++ b/Runtime/Authoring/Hybrid/NetCodeServerSettings.cs
@@ -1,6 +1,5 @@
#if UNITY_EDITOR
using System;
-using System.IO;
using System.Linq;
using UnityEngine;
using Unity.Entities.Build;
@@ -9,36 +8,78 @@
using UnityEngine.UIElements;
using Hash128 = Unity.Entities.Hash128;
-namespace Authoring.Hybrid
+namespace Unity.NetCode.Hybrid
{
- internal class NetCodeServerSettings : DotsPlayerSettings
+ [FilePath("ProjectSettings/NetCodeServerSettings.asset", FilePathAttribute.Location.ProjectFolder)]
+ internal class NetCodeServerSettings : ScriptableSingleton, IEntitiesPlayerSettings, INetCodeConversionTarget
{
- [SerializeField]
- public NetcodeConversionTarget NetcodeTarget = NetcodeConversionTarget.Server;
+ NetcodeConversionTarget INetCodeConversionTarget.NetcodeTarget => NetcodeConversionTarget.Server;
[SerializeField]
public BakingSystemFilterSettings FilterSettings;
[SerializeField] public string[] AdditionalScriptingDefines = Array.Empty();
- public override BakingSystemFilterSettings GetFilterSettings()
+ static Entities.Hash128 s_Guid;
+ public Entities.Hash128 GUID
+ {
+ get
+ {
+ if (!s_Guid.IsValid)
+ s_Guid = UnityEngine.Hash128.Compute(GetFilePath());
+ return s_Guid;
+ }
+ }
+
+ public string CustomDependency => GetFilePath();
+ void IEntitiesPlayerSettings.RegisterCustomDependency()
+ {
+ var hash = GetHash();
+ AssetDatabase.RegisterCustomDependency(CustomDependency, hash);
+ }
+
+ public UnityEngine.Hash128 GetHash()
+ {
+ var hash = (UnityEngine.Hash128)GUID;
+ if (FilterSettings?.ExcludedBakingSystemAssemblies != null)
+ foreach (var assembly in FilterSettings.ExcludedBakingSystemAssemblies)
+ {
+ var guid = AssetDatabase.GUIDFromAssetPath(AssetDatabase.GetAssetPath(assembly.asset));
+ hash.Append(ref guid);
+ }
+ foreach (var define in AdditionalScriptingDefines)
+ hash.Append(define);
+ return hash;
+ }
+
+ public BakingSystemFilterSettings GetFilterSettings()
{
return FilterSettings;
}
- public override string[] GetAdditionalScriptingDefines()
+ public string[] GetAdditionalScriptingDefines()
{
return AdditionalScriptingDefines;
}
+
+ public ScriptableObject AsScriptableObject()
+ {
+ return instance;
+ }
+
+ internal void Save()
+ {
+ Save(true);
+ ((IEntitiesPlayerSettings)this).RegisterCustomDependency();
+ if (!AssetDatabase.IsAssetImportWorkerProcess())
+ AssetDatabase.Refresh();
+ }
+ private void OnDisable() { Save(); }
}
internal class ServerSettings : DotsPlayerSettingsProvider
{
- private NetCodeServerSettings m_NetCodeServerSettings;
-
- private VisualElement m_rootElement;
-
- private Hash128 m_ServerGUID;
+ VisualElement m_BuildSettingsContainer;
public override int Importance
{
@@ -50,52 +91,30 @@ public override DotsGlobalSettings.PlayerType GetPlayerType()
return DotsGlobalSettings.PlayerType.Server;
}
- public override Hash128 GetPlayerSettingGUID()
- {
- if(!m_ServerGUID.IsValid)
- LoadOrCreateServerAsset();
- return m_ServerGUID;
- }
-
- public override DotsPlayerSettings GetSettingAsset()
+ protected override Hash128 DoGetPlayerSettingGUID()
{
- if (m_NetCodeServerSettings == null)
- LoadOrCreateServerAsset();
- return m_NetCodeServerSettings;
- }
-
- void LoadOrCreateServerAsset()
- {
- var path = k_DefaultAssetPath + k_DefaultAssetName + "ServerSettings" + k_DefaultAssetExtension;
- if(File.Exists(path))
- m_NetCodeServerSettings = AssetDatabase.LoadAssetAtPath(path);
- else
- {
- m_NetCodeServerSettings = (NetCodeServerSettings)ScriptableObject.CreateInstance(typeof(NetCodeServerSettings));
- m_NetCodeServerSettings.name = k_DefaultAssetName + nameof(ServerSettings);
-
- AssetDatabase.CreateAsset(m_NetCodeServerSettings, path);
- }
- m_ServerGUID = new Hash128(AssetDatabase.AssetPathToGUID(path));
+ return NetCodeServerSettings.instance.GUID;
}
- public override void Enable(int value)
+ protected override IEntitiesPlayerSettings DoGetSettingAsset()
{
- m_rootElement.SetEnabled((value == (int)DotsGlobalSettings.PlayerType.Server));
+ return NetCodeServerSettings.instance;
}
public override void OnActivate(DotsGlobalSettings.PlayerType type, VisualElement rootElement)
{
- m_rootElement = new VisualElement();
- m_rootElement.AddToClassList("target");
- m_rootElement.SetEnabled(type == DotsGlobalSettings.PlayerType.Server);
+ rootElement.RegisterCallback(OnAttachToPanel);
+ rootElement.RegisterCallback(OnDetachFromPanel);
+
+ m_BuildSettingsContainer = new VisualElement();
+ m_BuildSettingsContainer.AddToClassList("target");
- var so = new SerializedObject(GetSettingAsset());
- m_rootElement.Bind(so);
+ var so = new SerializedObject(NetCodeServerSettings.instance);
+ m_BuildSettingsContainer.Bind(so);
so.Update();
var label = new Label("Server");
- m_rootElement.Add(label);
+ m_BuildSettingsContainer.Add(label);
var targetS = new VisualElement();
targetS.AddToClassList("target-Settings");
@@ -109,12 +128,26 @@ public override void OnActivate(DotsGlobalSettings.PlayerType type, VisualElemen
propExtraDefinesField.name = "Extra Defines";
targetS.Add(propExtraDefinesField);
- m_rootElement.Add(targetS);
- rootElement.Add(m_rootElement);
+ m_BuildSettingsContainer.Add(targetS);
+ rootElement.Add(m_BuildSettingsContainer);
so.ApplyModifiedProperties();
}
+ static void OnAttachToPanel(AttachToPanelEvent evt)
+ {
+ // The ScriptableSingleton is not directly editable by default.
+ // Change the hideFlags to make the SerializedObject editable.
+ NetCodeServerSettings.instance.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
+ }
+
+ static void OnDetachFromPanel(DetachFromPanelEvent evt)
+ {
+ // Restore the original flags
+ NetCodeServerSettings.instance.hideFlags = HideFlags.HideAndDontSave;
+ NetCodeServerSettings.instance.Save();
+ }
+
public override string[] GetExtraScriptingDefines()
{
return new []{"UNITY_SERVER"}.Concat(GetSettingAsset().GetAdditionalScriptingDefines()).ToArray();
diff --git a/Runtime/Authoring/Hybrid/PreSpawnedGhostsBakingSystem.cs b/Runtime/Authoring/Hybrid/PreSpawnedGhostsBakingSystem.cs
index ae1400e..c4a4f8a 100644
--- a/Runtime/Authoring/Hybrid/PreSpawnedGhostsBakingSystem.cs
+++ b/Runtime/Authoring/Hybrid/PreSpawnedGhostsBakingSystem.cs
@@ -57,7 +57,11 @@ protected override void OnUpdate()
// TODO: Check the interpolated/predicted/server bools instead
// Only iterate ghostAuthoring.Components
// Should skip PhysicsCollider, WorldRenderBounds, XXXSnapshotData, PredictedGhostComponent
+#if !ENABLE_TRANSFORM_V1
+ if (componentTypes[i] == typeof(LocalTransform))
+#else
if (componentTypes[i] == typeof(Translation) || componentTypes[i] == typeof(Rotation))
+#endif
{
var componentDataHash = ComponentDataToHash(entity, componentTypes[i]);
hashData.Add(componentDataHash);
@@ -167,7 +171,7 @@ ulong ComponentDataToHash(Entity entity, ComponentType componentType)
var untypedType = EntityManager.GetDynamicComponentTypeHandle(componentType);
var chunk = EntityManager.GetChunk(entity);
var sizeInChunk = TypeManager.GetTypeInfo(componentType.TypeIndex).SizeInChunk;
- var data = chunk.GetDynamicComponentDataArrayReinterpret(untypedType, sizeInChunk);
+ var data = chunk.GetDynamicComponentDataArrayReinterpret(ref untypedType, sizeInChunk);
var entityType = GetEntityTypeHandle();
var entities = chunk.GetNativeArray(entityType);
diff --git a/Runtime/Authoring/Hybrid/Unity.NetCode.Authoring.Hybrid.asmdef b/Runtime/Authoring/Hybrid/Unity.NetCode.Authoring.Hybrid.asmdef
index 8995f21..e2760b5 100644
--- a/Runtime/Authoring/Hybrid/Unity.NetCode.Authoring.Hybrid.asmdef
+++ b/Runtime/Authoring/Hybrid/Unity.NetCode.Authoring.Hybrid.asmdef
@@ -24,6 +24,32 @@
"defineConstraints": [
"!UNITY_DOTSRUNTIME"
],
- "versionDefines": [],
+ "versionDefines": [
+ {
+ "name": "com.unity.cinemachine.dots",
+ "expression": "0.60.0-preview.81",
+ "define": "ENABLE_TRANSFORM_V1"
+ },
+ {
+ "name": "com.unity.stableid",
+ "expression": "0.60.0-preview.91",
+ "define": "ENABLE_TRANSFORM_V1"
+ },
+ {
+ "name": "com.unity.2d.entities.physics",
+ "expression": "0.5.0-preview.1",
+ "define": "ENABLE_TRANSFORM_V1"
+ },
+ {
+ "name": "com.unity.environment",
+ "expression": "0.2.0-preview.17",
+ "define": "ENABLE_TRANSFORM_V1"
+ },
+ {
+ "name": "com.unity.platforms",
+ "expression": "",
+ "define": "USING_PLATFORMS_PACKAGE"
+ }
+ ],
"noEngineReferences": false
}
diff --git a/Runtime/ClientServerWorld/ClientServerBootstrap.cs b/Runtime/ClientServerWorld/ClientServerBootstrap.cs
index bbc5390..26d62f3 100644
--- a/Runtime/ClientServerWorld/ClientServerBootstrap.cs
+++ b/Runtime/ClientServerWorld/ClientServerBootstrap.cs
@@ -46,7 +46,7 @@ public ClientServerBootstrap()
///
/// The name to use for the default world.
/// A new world instance.
- public World CreateLocalWorld(string defaultWorldName)
+ public static World CreateLocalWorld(string defaultWorldName)
{
// The default world must be created before generating the system list in order to have a valid TypeManager instance.
// The TypeManage is initialised the first time we create a world.
diff --git a/Runtime/ClientServerWorld/ClientServerTickRate.cs b/Runtime/ClientServerWorld/ClientServerTickRate.cs
index 4edf3e6..82d2e8c 100644
--- a/Runtime/ClientServerWorld/ClientServerTickRate.cs
+++ b/Runtime/ClientServerWorld/ClientServerTickRate.cs
@@ -102,9 +102,9 @@ public void ResolveDefaults()
if (NetworkTickRate <= 0)
NetworkTickRate = SimulationTickRate;
if (MaxSimulationStepsPerFrame <= 0)
- MaxSimulationStepsPerFrame = 4;
+ MaxSimulationStepsPerFrame = 1;
if (MaxSimulationStepBatchSize <= 0)
- MaxSimulationStepBatchSize = 1;
+ MaxSimulationStepBatchSize = 4;
}
}
diff --git a/Runtime/Command/CommandReceiveSystem.cs b/Runtime/Command/CommandReceiveSystem.cs
index 4cece0c..f8dca02 100644
--- a/Runtime/Command/CommandReceiveSystem.cs
+++ b/Runtime/Command/CommandReceiveSystem.cs
@@ -31,6 +31,7 @@ public void OnCreate(ref SystemState state)
public void OnDestroy(ref SystemState state)
{}
+ [BurstCompile]
partial struct CommandReceiveClearJob : IJobEntity
{
public NetworkTick _currentTick;
@@ -194,10 +195,10 @@ public struct ReceiveJobData
///
public void Execute(ArchetypeChunk chunk, int orderIndex)
{
- var snapshotAcks = chunk.GetNativeArray(snapshotAckType);
- var networkIds = chunk.GetNativeArray(networkIdType);
- var commandTargets = chunk.GetNativeArray(commmandTargetType);
- var cmdBuffers = chunk.GetBufferAccessor(cmdBufferType);
+ var snapshotAcks = chunk.GetNativeArray(ref snapshotAckType);
+ var networkIds = chunk.GetNativeArray(ref networkIdType);
+ var commandTargets = chunk.GetNativeArray(ref commmandTargetType);
+ var cmdBuffers = chunk.GetBufferAccessor(ref cmdBufferType);
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
{
diff --git a/Runtime/Command/CommandSendSystem.cs b/Runtime/Command/CommandSendSystem.cs
index 0f5c8bd..b9aec5b 100644
--- a/Runtime/Command/CommandSendSystem.cs
+++ b/Runtime/Command/CommandSendSystem.cs
@@ -87,7 +87,7 @@ protected override void OnCreate()
}
protected override void OnUpdate()
{
- var clientNetTime = GetSingleton();
+ var clientNetTime = SystemAPI.GetSingleton();
var targetTick = NetworkTimeHelper.LastFullServerTick(clientNetTime);
// Make sure we only send a single ack per tick - only triggers when using dynamic timestep
if (targetTick == m_lastServerTick)
@@ -363,9 +363,9 @@ void Serialize(DynamicBuffer rpcData,
/// unsed, the sorting index enequeing operation in the the entity command buffer
public void Execute(ArchetypeChunk chunk, int orderIndex)
{
- var commandTargets = chunk.GetNativeArray(commmandTargetType);
- var networkIds = chunk.GetNativeArray(networkIdType);
- var rpcDatas = chunk.GetBufferAccessor(outgoingCommandBufferType);
+ var commandTargets = chunk.GetNativeArray(ref commmandTargetType);
+ var networkIds = chunk.GetNativeArray(ref networkIdType);
+ var rpcDatas = chunk.GetBufferAccessor(ref outgoingCommandBufferType);
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
{
diff --git a/Runtime/Command/IInputComponentData.cs b/Runtime/Command/IInputComponentData.cs
index 2262c9a..fc17dae 100644
--- a/Runtime/Command/IInputComponentData.cs
+++ b/Runtime/Command/IInputComponentData.cs
@@ -108,9 +108,9 @@ public struct CopyInputToBufferJob
[BurstCompile]
public void Execute(ArchetypeChunk chunk, int orderIndex)
{
- var inputs = chunk.GetNativeArray(InputDataType);
- var owners = chunk.GetNativeArray(GhostOwnerDataType);
- var inputBuffers = chunk.GetBufferAccessor(InputBufferDataType);
+ var inputs = chunk.GetNativeArray(ref InputDataType);
+ var owners = chunk.GetNativeArray(ref GhostOwnerDataType);
+ var inputBuffers = chunk.GetBufferAccessor(ref InputBufferDataType);
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
{
@@ -226,8 +226,8 @@ public struct ApplyInputDataFromBufferJob
[BurstCompile]
public void Execute(ArchetypeChunk chunk, int orderIndex)
{
- var inputs = chunk.GetNativeArray(InputDataType);
- var inputBuffers = chunk.GetBufferAccessor(InputBufferTypeHandle);
+ var inputs = chunk.GetNativeArray(ref InputDataType);
+ var inputBuffers = chunk.GetBufferAccessor(ref InputBufferTypeHandle);
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
{
diff --git a/Runtime/Connection/DefaultDriverConstructor.cs b/Runtime/Connection/DefaultDriverConstructor.cs
index e07fc52..b02841f 100644
--- a/Runtime/Connection/DefaultDriverConstructor.cs
+++ b/Runtime/Connection/DefaultDriverConstructor.cs
@@ -196,7 +196,7 @@ bool FoundServerWorldInstance()
///
/// Register a NetworkDriver instance in :
/// - a single NetworkDriver if the both client and server worlds are present in the same process
- /// - a single driver in all other cases
+ /// - a single driver in all other cases
/// These are configured using internal defaults. See: .
///
/// Used for determining whether we are running in a client or server world.
@@ -210,7 +210,7 @@ public static void RegisterClientDriver(World world, ref NetworkDriverStore driv
///
/// Register a NetworkDriver instance in and stores it in :
/// - a single NetworkDriver if the both client and server worlds are present in the same process.
- /// - a single driver in all other cases.
+ /// - a single driver in all other cases.
/// These are configured using the NetworkSettings passed in.
///
/// Used for determining whether we are running in a client or server world.
@@ -243,8 +243,8 @@ public static void RegisterClientDriver(World world, ref NetworkDriverStore driv
///
/// Register a NetworkDriver instance in :
- /// both and NetworkDriver in the editor and only
- /// a single driver in the build.
+ /// both and NetworkDriver in the editor and only
+ /// a single driver in the build.
/// These are configured using internal defaults. See: .
///
/// Used for determining whether we are running in a client or server world.
@@ -258,8 +258,8 @@ public static void RegisterServerDriver(World world, ref NetworkDriverStore driv
///
/// Register a NetworkDriver instance in :
- /// both and NetworkDriver in the editor and only
- /// a single driver in the build.
+ /// both and NetworkDriver in the editor and only
+ /// a single driver in the build.
/// These are configured using the NetworkSettings passed in.
///
/// Used for determining whether we are running in a client or server world.
@@ -326,7 +326,7 @@ public static void CreateClientSimulatorPipelines(ref NetworkDriverStore.Network
///
/// Register a NetworkDriver instance in and stores it in :
/// - a single NetworkDriver if the both client and server worlds are present in the same process.
- /// - a single driver in all other cases.
+ /// - a single driver in all other cases.
/// These are configured using the default settings. See .
///
/// Used for determining whether we are running in a client or server world.
@@ -343,8 +343,8 @@ public static void RegisterClientDriver(World world, ref NetworkDriverStore driv
///
/// Register a NetworkDriver instance in :
- /// both and NetworkDriver in the editor and only
- /// a single driver in the build.
+ /// both and NetworkDriver in the editor and only
+ /// a single driver in the build.
/// These are configured using the default settings. See .
///
/// Used for determining whether we are running in a client or server world.
@@ -363,7 +363,7 @@ public static void RegisterServerDriver(World world, ref NetworkDriverStore driv
///
/// Register a NetworkDriver instance in and stores it in :
/// - a single NetworkDriver if the both client and server worlds are present in the same process.
- /// - a single driver in all other cases.
+ /// - a single driver in all other cases.
/// These are configured using the default settings. See .
///
/// Used for determining whether we are running in a client or server world.
@@ -379,8 +379,8 @@ public static void RegisterClientDriver(World world, ref NetworkDriverStore driv
///
/// Register a NetworkDriver instance in :
- /// both and NetworkDriver in the editor and only
- /// a single driver in the build.
+ /// both and NetworkDriver in the editor and only
+ /// a single driver in the build.
/// These are configured using the default settings. See .
///
/// Used for determining whether we are running in a client or server world.
@@ -398,12 +398,12 @@ public static void RegisterServerDriver(World world, ref NetworkDriverStore driv
///
/// The default NetCode driver constructor. It creates:
- /// - On the server: both and NetworkDriver in the editor and only
- /// a single driver in the build.
+ /// - On the server: both and NetworkDriver in the editor and only
+ /// a single driver in the build.
/// - On the client:
/// - a single NetworkDriver if the both client and server worlds are present in the same process.
- /// - a single driver in all other cases.
- /// In the Editor and Development build, if the network simulator is enabled, force on the client to use the network driver.
+ /// - a single driver in all other cases.
+ /// In the Editor and Development build, if the network simulator is enabled, force on the client to use the network driver.
///
/// To let the client use the IPC network interface In ClientServer mode it is mandatory to always create the server world first.
///
diff --git a/Runtime/Connection/NetworkStreamReceiveSystem.cs b/Runtime/Connection/NetworkStreamReceiveSystem.cs
index cce25a2..64110be 100644
--- a/Runtime/Connection/NetworkStreamReceiveSystem.cs
+++ b/Runtime/Connection/NetworkStreamReceiveSystem.cs
@@ -60,6 +60,8 @@ public unsafe partial struct NetworkStreamConnectSystem : ISystem
private EntityQuery m_ConnectionRequestConnectQuery;
private ComponentLookup m_NetworkStreamRequestConnectFromEntity;
private ComponentLookup m_ConnectionStateFromEntity;
+
+ [BurstCompile]
public void OnCreate(ref SystemState state)
{
m_ConnectionRequestConnectQuery = state.GetEntityQuery(ComponentType.ReadWrite());
@@ -69,9 +71,13 @@ public void OnCreate(ref SystemState state)
state.RequireForUpdate();
state.RequireForUpdate();
}
+
+ [BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
+
+ [BurstCompile]
public void OnUpdate(ref SystemState systemState)
{
var netDebug = SystemAPI.GetSingleton