diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b56f59..be4717d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,25 @@ # Change log +## [0.0.3-preview.2] - 2019-12-05 +### New features +### Changes +* Updated the documentation and added a section about prediction. +* Upgraded entities to 0.3.0. + +### Fixes +* Fixed a crash when multiple clients disconnected on the same frame. +* Fixed read / write access specifiers in AfterSimulationInterpolationSystem. +* Fixed build errors in non-development standalone builds. + +### Upgrade guide + ## [0.0.2-preview.1] - 2019-11-28 ### New features ### Changes ### Fixes * Fix compile error in generated serialization code for strings. * Fix warning when entering playmode with the netcode disabled. + ### Upgrade guide ## [0.0.1-preview.6] - 2019-11-26 @@ -19,6 +33,7 @@ * Added a protocol version which must match for the connection to succeed. * Added time graphs and server view to the network debugger. * Network simulator now supports jitter. + ### Changes * The authoring flow has been improved. * `GhostAuthoringComponent` now automatically detects what components an entity has after conversion runs and automatically populates them when you press the "Update component list" button. You no longer need to manually type in each component name. diff --git a/Documentation~/TableOfContents.md b/Documentation~/TableOfContents.md new file mode 100644 index 0000000..3c60cbd --- /dev/null +++ b/Documentation~/TableOfContents.md @@ -0,0 +1,9 @@ +* [About Unity NetCode](index.md) +* [Getting Started](getting-started.md) +* [Client server Worlds](client-server-worlds.md) +* [Network connection](network-connection.md) +* [RPCs](rpcs.md) +* [Command stream](command-stream.md) +* [Ghost snapshots](ghost-snapshots.md) +* [Prediction](prediction.md) +* [Time synchronization](time-synchronization.md) \ No newline at end of file diff --git a/Documentation~/client-server-worlds.md b/Documentation~/client-server-worlds.md new file mode 100644 index 0000000..6233047 --- /dev/null +++ b/Documentation~/client-server-worlds.md @@ -0,0 +1,77 @@ +# Client server Worlds + +NetCode has a separation of client and server logic, and both the client and server logic are in separate Worlds (the client World, and the server World), based on the [hierarchical update system](https://docs.unity3d.com/Packages/com.unity.entities@latest/index.html?subfolder=/manual/system_update_order.html) of Unity’s Entity Component System (ECS). + +By default, NetCode places systems in both client and server Worlds, but not in the default World. **Note:** Systems that update in the `PresentationSystemGroup` are only added to the client World. + +To override this default behavior, use the [UpdateInWorld](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.UpdateInWorld.html) attribute, or the `UpdateInGroup` attribute with an explicit client or server system group. The available explicit client server groups are as follows: + +* [ClientInitializationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientInitializationSystemGroup.html) +* [ServerInitializationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ServerInitializationSystemGroup.html) +* [ClientAndServerInitializationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientAndServerInitializationSystemGroup.html) +* [ClientSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientSimulationSystemGroup.html) +* [ServerSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ServerSimulationSystemGroup.html) +* [ClientAndServerSimulationSystemGroup ](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientAndServerSimulationSystemGroup.html) +* [ClientPresentationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientPresentationSystemGroup.html) + +**Note:** There is no server presentation system group. + +As well as the attributes listed above, you can use the __PlayMode Tools__ window in the Unity Editor to select what happens when you enter Play Mode. To access __PlayMode Tools__, go to menu: __Multiplayer > PlayMode Tools__. + +![PlayMode Tools](/images/playmode-tools.png)
_PlayMode Tools_ + +|**Property**|**Description**| +|:---|:---| +|__PlayMode Type__|Choose to make Play Mode either __Client__ only, __Server__ only, or __Client & Server__.| +|__Num Clients__|Set the number of clients to run in the same process. You can run several clients in Play mode, all in separate Worlds.| +|__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.| +|__Client send/recv delay__|Use this property to emulate high ping. Specify a time (in ms) to delay each outgoing and incoming network packet by. | +|__Client send/recv jitter__|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 __Client send/recv delay__ to 45 and __Client send/recv jitter__ to 5, you will get a random value between 40 and 50.| +|__Client package drop__|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 discard 5% of all incoming and outgoing packets.| +|__Client auto connect address (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. | + +When you enter Play Mode, from this window you can also disconnect clients and choose which client Unity should present if there are multiple. 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. + +## Bootstrap + +The default bootstrap creates client server Worlds automatically at startup. It populates them with the systems defined in the attributes you have set. This is useful when you are working in the Editor, but in a standalone game, you might want to delay the World creation so you can use the same executable as both a client and server. + +To do this, you can create a class that extends `ClientServerBootstrap` to override the default bootstrap. Implement `Initialize` and create the default World. To create the client and server worlds manually call `ClientServerBootstrap.CreateClientWorld(defaultWorld, "WorldName");` or `ClientServerBootstrap.CreateServerWorld(defaultWorld, "WorldName");`. + +The following code example shows how to override the default bootstrap to prevent automatic creation of the client server worlds: + +```c# +public class ExampleBootstrap : ClientServerBootstrap +{ + public override bool Initialize(string defaultWorldName) + { + var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default); + GenerateSystemLists(systems); + + var world = new World(defaultWorldName); + World.DefaultGameObjectInjectionWorld = world; + + DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, ExplicitDefaultWorldSystems); + ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world); + return true; + } + +} +``` + +## Fixed and dynamic timestep + +When you use NetCode, the server always updates at a fixed timestep. NetCode limits the maximum number of iterations to make sure that the server does not end up in a state where it takes several seconds to simulate a single frame. + +The fixed update does not use the [standard Unity update frequency](https://docs.unity3d.com/Manual/class-TimeManager.html). A singleton entity in the server World controls the update with a [ClientServerTickRate](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerTickRate.html) component. The `ClientServerTickRate` controls `SimulationTickRate` which sets the number of simulation ticks per second. **Note:** `SimulationTickRate` must be divisible by `NetworkTickRate`. + +The default number of simulation ticks is 60. The component also has values for MaxSimulationStepsPerFrame which controls how many simulations the server can run in a single frame, and TargetFrameRateMode which controls how the server should keep the tick rate. Available values are: +* `BusyWait` to run at maximum speed, +* `Sleep` for `Application.TargetFrameRate` to reduce CPU load +* `Auto` to use `Sleep` on headless servers and `BusyWait` otherwise + +By default, the client updates at a dynamic time step, with the exception of prediction code which always runs at a fixed time step to match the server. The prediction runs in the [GhostPredictionSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostPredictionSystemGroup.html) and applies its own fixed time step for prediction. To make the client use a fixed time step, create a singleton entry with the `FixedClientTickRate` component. If you use the fixed tick rate, you can add a `CurrentSimulatedPosition` and `CurrentSimulatedRotation` component to an entity to handle render interpolation. + +## Standalone builds + +When you build a standalone game, NetCode uses the __Server Build__ property in the __Build Settings__ window to decide what to build. If the property is enabled, NetCode sets the ```UNITY_SERVER``` define and you get a server-only build. If the property is disabled you get a combined client and server build. You can use a combined client and server build to decide if a game should be client, server or both at runtime. To build a client-only game, add the ```UNITY_CLIENT``` define to the __Scripting Define Symbols__ in the __Player Settings__ (menu: __Edit > Project Settings > Player > Configuration__). You can have the ```UNITY_CLIENT``` define set when you build a server, but the ```UNITY_SERVER``` define takes precedence and you get a server-only build. diff --git a/Documentation~/command-stream.md b/Documentation~/command-stream.md new file mode 100644 index 0000000..473511e --- /dev/null +++ b/Documentation~/command-stream.md @@ -0,0 +1,17 @@ +# Command stream + +The client continuously sends a command stream to the server. This stream includes all inputs and acknowledgements of the last received snapshot. When no commands are sent a [NullCommandSendSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NullCommandSendSystem.html) sends acknowledgements for received snapshots without any inputs. This is an automatic system to make sure the flow works automatically when the game does not need to send any inputs. + +To create a new input type, create a struct that implements the `ICommandData` interface. To implement that interface you need to provide methods for accessing the `Tick` as well as `Serialize` and `Deserialize`. + +[ICommandData](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ICommandData-1.html) has two __Serialize__ and two __Deserialize__ methods: one pair for raw values, and one pair for delta compressed values. The system sends multiple inputs in each command packet. The first packet contains raw data but the rest are compressed using delta compression. Delta compression compresses inputs well because the rate of change is low. + +As well as creating a struct you need to create specific instances of the generic systems `CommandSendSystem` and `CommandReceiveSystem`. To do this, extend the base system, for example with `class MyCommandSendSystem : CommandSendSystem{}.` + +As well as setting the input buffer on an entity, your game code must also set the [CommandTargetComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.CommandTargetComponent.html) on the connection entity to reference the entity that the `ICommandData` component has been attached to. + +You can have multiple command systems, and NetCode selects the correct one based on the `ICommandData` type of the entity that points to `CommandTargetComponent`. + +When you need to access inputs on the client and server, it is important to read the data from the `ICommandData` rather than reading it directly from the system. If you read the data from the system the inputs won’t match between the client and server so your game will not behave as expected. + +When you need to access the inputs from the buffer, you can use an extension method for `DynamicBuffer` called `GetDataAtTick` which gets the matching tick for a specific frame. You can also use the `AddCommandData` utility method which adds more commands to the buffer. \ No newline at end of file diff --git a/Documentation~/quickstart.md b/Documentation~/getting-started.md similarity index 55% rename from Documentation~/quickstart.md rename to Documentation~/getting-started.md index d615cc1..9c7dea3 100644 --- a/Documentation~/quickstart.md +++ b/Documentation~/getting-started.md @@ -1,36 +1,32 @@ -# DOTS NetCode quick start guide -This doc is a walkthrough of how to create a very simple client / server based simulation. We will be spawning and controlling a simple Prefab. +# Getting started with NetCode +This documentation provides a walkthrough of how to create a very simple client server based simulation. This walkthrough describes how to spawn and control a simple Prefab. -## Setup the project -The first step is to create a new project. From the hub we create a new project called "NetCube". +## Set up the Project +Open the __Unity Hub__ and create a new Project. **Note:** To use Unity NetCode you must have at least Unity 2019.3.b11 installed. -> NOTE: `com.unity.netcode` requires at least `Unity 2019.3.b11` +Open the Package Manager (menu: __Window > Package Manager__). At the top of the window, under __Advanced__, select __Show preview packages__. Add the Entities, Hybrid Renderer, NetCode, and Transport packages. -![NewProject](new-project.png "New Project") +## Create an initial Scene -We need to add some packages to the new project, so open `Window > Package Manager`, make sure "Show preview packages" under "Advances is enabled and add the packages `Transport`, `NetCode`, `Entities`and `Hybrid Renderer`. +To begin, you need to set up a way to share data between the client and the server. To achieve this separation in NetCode, you need to create a different World for each client and the server. To share data between the server and the client, create an empty GameObject (called __SharedData__ in the example), and add the __ConvertToClientServerEntity__ component. -## Create the initial scene +![Empty SharedData GameObject](images/world-game-objects.png)
_Empty SharedData GameObject_ -To start off we want a way to share data between the client and the server. In NetCode we achieve the seperation by creating a different world for each client and the server. +![ConvertToClientServerEntity component](images/mixed-world.png)
_ConvertToClientServerEntity component_ -To share data between the server and the client we need create a empty GameObject and add the `ConvertToClientServerEntity` Component. +Once you set this up you can, for example, spawn a plane in both the client and the server world. To do this, right click the __SharedData__ Prefab and select __3D Object > Plane__ which then creates a plane that is nested under __SharedData__. -![WorldGameObjects](world-game-objects.png "World Game Objects") +![Scene with a plane](images/initial-scene.png)
_Scene with a plane_ -![MixedWorld](mixed-world.png "MixedWorld") +## Create a ghost Prefab -For example if we want to spawn a plane in both the client and the server world we can just simply create a plane under `SharedData`, right click `SharedData` and select `3D Object > Plane`. +To make your Scene run with a client / server setup you need to create a definition of the networked object, which is called a **ghost**. -![InitialScene](initial-scene.png "Initial Scene") +To create a ghost Prefab, create a cube in the Scene (right click on the Scene and select __3D Object > Cube__). Then select the Cube GameObject under the Scene and drag it into the Project’s __Asset__ folder. This creates a Prefab of the Cube. -## Create the ghost prefab +![Create a Cube Prefab](images/cube-prefab.png)
_Create a Cube Prefab_ -To make this scene run with a client / server setup we need to create a definition of the networked object - called a ghost. We start by creating a cube in our scene, e.g. `3D Object > Cube`. Then we need to create a prefab by dragging the "Cube" GameObject to the Assets panel in Unity. - -![CubePrefab](cube-prefab.png "Cube Prefab") - -We want to be able to indentify our Player and in Netcode we usually do that by PlayerId. So lets start by creating and adding a simple component that we will use to identify our cube with. +In NetCode you identify the player with its PlayerId. To identify the Cube Prefab, create a simple component with the following code: ```c# using Unity.Entities; @@ -44,25 +40,26 @@ public struct MovableCubeComponent : IComponentData } ``` -Then add the `MovableCubeComponent` to our `Cube` Prefab. +Once you create this component, add it to the Cube Prefab. Then, in the Inspector, add the __Ghost Authoring Component__ to the Prefab. In this component, select __Update Component List__ to update the list of components. + +When you do this, Unity automatically adds default values to the Translation and Rotation components. Expand the components in the list to select where they should be present. In this example, disable `PerInstanceCullingTag` and `RenderMesh` on the server. This is because the server does not render anything, and interpolated objects on the client don’t simulate anything. -Next we add a `Ghost Authoring Component` to the prefab. In the inspector for the `Ghost Authoring Component` we can update the list of components by clicking "Update component list". The Translation and Rotation components should get values setup by default. All we need to do is expand the components in this list and select where they should be present. In our case we want to disable `PerInstanceCullingTag` and `RenderMesh` on the server. This is because the server is not supposed to render anything, and interpolated objects on the client are not supposed to simulate anything. +![The Ghost Authoring component](images/ghost-config.png)
_The Ghost Authoring component_ -Make sure to change the `Default Client Instantiation` to `Owner Predicted` this will make sure that we predict our own movement. +**Note:** Change the __Default Client Instantiation__ to __Owner Predicted__. This makes sure that you predict your own movement. -![GhostConfig](ghost-config.png "Ghost Config") +After you set up the component, select the __Generate Code__ button. -With this in place we click "Generate Code" in the `Ghost Authoring Component`. +## Hook up the collections +To tell NetCode which Ghosts to use, set up a GhostCollection. Because both the client and the server need to know about these Ghosts, add it to the __SharedData__ Scene. Right click on SharedData and select __Create Empty__. Rename it to __GhostCollection__ and then add a __GhostCollectionAuthoringComponent__. -## Hooking up the collections -In order for the NetCode to know which Ghosts to use we need to setup a GhostCollection. Because both the client and the server need to know about these Ghosts we create it under our SharedData. Right click on `SharedData` and chose `Create Empty`. I will rename it to GhostCollection and then add a `GhostCollectionAuthoringComponent`. +In the Inspector select the __Update ghost list__ button and then the __Generate collection code__ button. -![GhostConllection](ghost-collection.png "Ghost Collection") +![Ghost Collection settings](images/ghost-collection.png)
_Ghost Collection settings_ -In the `Inspector` click on the "Update ghost list" and then "Generate collection code". +## Establish a connection +Next, you need to make sure the server starts listening for connections, the client connects, and all connections are marked as "in game" so NetCode can start sending snapshots. You don’t need a full flow in this case, write the minimal amount of code to set it up. Create a file called *Game.cs* under __Assets__ and the following code to the file: -## Establishing a connection -The next thing we need to do is make sure the server starts listening for connections, the client connects and all connections are marked as "in game" so the NetCode start sending snapshots. We do not need a full flow in this case, so we will just write the minimal amount of code to set it up. Create a file called `Game.cs` under Assets and add this code to it. ```c# using System; using Unity.Entities; @@ -102,7 +99,7 @@ public class Game : ComponentSystem #if UNITY_EDITOR else if (world.GetExistingSystem() != null) { - // Server world automatically listen for connections from any host + // Server world automatically listens for connections from any host NetworkEndPoint ep = NetworkEndPoint.AnyIpv4; ep.Port = 7979; network.Listen(ep); @@ -113,9 +110,9 @@ public class Game : ComponentSystem } ``` -Next we need a way to tell the server we are ready to start playing. To do this we can use `Rpc` calls that are availeble in the NetCode package. +Next you need to tell the server you are ready to start playing. To do this, use the `Rpc` calls that are available in the NetCode package. -We can continue in our `Game.cs` by first creating a `RpcCommand` that we will use to tell the server we are ready to start playing now. +In *Game.cs*, create the following [RpcCommand](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.IRpcCommand.html) that tells the server you are ready to start playing now: ```c# [BurstCompile] @@ -141,9 +138,9 @@ public struct GoInGameRequest : IRpcCommand } ``` -> NOTE: Do not forget the `BurstCompile` Attribute on `InvokeExecute` +**Note:** Don’t forget the `BurstCompile` attribute on `InvokeExecute`. -And to make sure `NetCode` handles the command we also create a `RpcCommandRequestSystem` +To make sure NetCode handles the command, you need to create a [RpcCommandRequestSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.RpcCommandRequestSystem-1.html) as follows: ```c# // The system that makes the RPC request component transfer @@ -152,9 +149,7 @@ public class GoInGameRequestSystem : RpcCommandRequestSystem } ``` -Next lets make sure we can send input from the client to the server. To do this we create a `ICommandData` struct. This struct will be responsible serialize and deserialize our input data. - -We can create the script `CubeInput.cs` and write our CubeInput CommandData. +Next, you need to create an `ICommandData` struct to make sure you can send input from the client to the server. This struct is responsible for serializing and deserializing the input data. Create a script called *CubeInput.cs* and write the `CubeInput CommandData` as follows: ```c# public struct CubeInput : ICommandData @@ -190,9 +185,7 @@ public struct CubeInput : ICommandData } ``` -So our command stream will consist of the current `tick` and our `horizontal` and `vertical` movements. - -In the same fashion as `Rpc`, `ICommandData` also needs a few systems to handle the command. So lets add them. +The command stream consists of the current tick and the horizontal and vertical movements. In the same fashion as `Rpc`, you need to set up `ICommandData` with some systems to handle the command as follows: ```c# public class NetCubeSendCommandSystem : CommandSendSystem @@ -203,7 +196,7 @@ public class NetCubeReceiveCommandSystem : CommandReceiveSystem } ``` -And then we want to sample our input that we will send over the wire. So let's create a System for that to. +To sample the input, send it over the wire. To do this, create a System for it as follows: ```c# [UpdateInGroup(typeof(ClientSimulationSystemGroup))] @@ -247,7 +240,7 @@ public class SampleCubeInput : ComponentSystem } ``` -Finally we want to create a system that is able to read the `CommandData` and move the player. +Finally, create a system that can read the `CommandData` and move the player. ```c# [UpdateInGroup(typeof(GhostPredictionSystemGroup))] @@ -277,13 +270,9 @@ public class MoveCubeSystem : ComponentSystem } ``` - - ## Tie it together -One last vital part is missing and its the systems that will handle when we go in game on the client and what to do when a client connects on the server. - -The Client is simple enough, we want to be able to send a `Rpc` to the server when we have connected that we are ready to start playing. +The final step you need to do is to create the systems that handle when you go in-game on the client and what to do when a client connects on the server. You need to be able to send an `Rpc` to the server when you connect that tells it you are ready to start playing. ```c# // When client has a connection with network id, go in game and tell server to also go in game @@ -307,9 +296,7 @@ public class GoInGameClientSystem : ComponentSystem } ``` - - -And on the server we want to make sure that when we receive a `GoInGameRequest` we will create and spawn a `Cube` for that player. +On the server you need to make sure that when you receive a `GoInGameRequest`, you create and spawn a Cube for that player. ```c# // When server receives go in game request, go in game and delete request @@ -338,21 +325,18 @@ public class GoInGameServerSystem : ComponentSystem } ``` -And thats it. - ## Testing it -Now everything should be good to go. Open `Multiplayer > PlayMode Tools` and make sure "PlayMode Type" is set to "Client & Server". Enter playmode and you should see the cube spawn. By pressing `a`, `s`, `d`, `w` you should also be able to move the Cube around. +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 __A,S,D,__ and __W__ keys to move the Cube around. -So to recap: +To recap on this workflow: -1. We Created a `GameObject` to hold SharedData between the Client and the Server. By adding the `ConvertToClientServerEntity` `Component`. -2. We created our Prefab out of a simple `3D Cube` and added a `GhostAuthoringComponent` to it as well as the `MovableCubeComponent` to it. -3. We updated and generated code for the Ghost through the `GhostAuthoringComponent` Inspector view. -4. We created a GhostCollection by adding the `GhostCollectionAuthoringComponent` to a Empty GameObject. - 1. We Updated the ghost list and generated collection code. -5. We established a connection between the client and the server. -6. We wrote a `Rpc` to tell the server we are ready to play. -7. We wrote a `ICommandData` to be serialize Game Input. -8. We wrote a Client system to send a `Rpc` -9. We wrote a Server system to handle our incomming `Rpc`. \ No newline at end of file +1. Created a GameObject and added the __ConvertToClientServerEntity__ component to hold __SharedData__ between the client and the server. +1. Created a Prefab out of a simple 3D Cube and added a __GhostAuthoringComponent__ to it as well as the __MovableCubeComponent__. +1. Updated and generated code for the Ghost through the __GhostAuthoringComponent__ Inspector view. +1. Added the __GhostCollectionAuthoringComponent__ to an empty GameObject to create a GhostCollection. Updated the ghost list and generated collection code. +1. Established a connection between the client and the server. +1. Wrote an `Rpc` to tell the server you are ready to play. +1. Wrote an `ICommandData` to serialize game input. +1. Wrote a client system to send an `Rpc` +1. Wrote a server system to handle the incoming `Rpc`. \ No newline at end of file diff --git a/Documentation~/ghost-snapshots.md b/Documentation~/ghost-snapshots.md new file mode 100644 index 0000000..90d962d --- /dev/null +++ b/Documentation~/ghost-snapshots.md @@ -0,0 +1,79 @@ +# Ghost snapshots + +A ghost is a networked object that the server simulates. During every frame, the server sends a snapshot of the current state of all ghosts to the client. The client presents them, but cannot directly control or affect them because the server owns them. + +The ghost snapshot system synchronizes entities which exist on the server to all clients. To make it perform properly, the server processes per ECS chunk rather than per entity. On the receiving side the processing is done per entity. This is because it is not possible to process per chunk on both sides, and the server has more connections than clients. + +## Ghost authoring component +The ghost authoring component is based on specifying ghosts as Prefabs with the __GhostAuthoringComponent__ on them. The __GhostAuthoringComponent__ has a small editor which you can use to configure how NetCode synchronizes the various components and fields in the Prefab, as well as where the components will be available. + +When you add a __GhostAuthoringComponent__ to the Prefab, select the __Update component list__ button in the Inspector. This determines which entities the Prefab will have after Unity converts it, and then adds them to the component list. + +![Ghost Authoring Component](images/ghost-config.png)
_Ghost Authoring Component_ + +Unity uses the list to configure the ghost. You can enable or disable the properties in the Inspector (__Server, Interpolated Client, Predacted Client__) to select which version of the Prefab will have a component. For example, if you disable the __Server__ property from RenderMesh, the ghost won’t have a RenderMesh when it is instantiated on the server, but it will have it when instantiated on the client. To control the defaults for these checkboxes from source, add the `GhostDefaultComponentAttribute` to an `IComponentData`. + +For each component, you need to set the values to synchronize and their quality. To set up default values, add the `GhostDefaultFieldAttribute` attribute to the fields in an `IComponentData`. To override the default values for synchronization for a specific ghost, enable the __Manual Field List__ property and edit them. + +As well as adding attributes, you can override the defaults for components which you do not have source access to by creating an entry to `GhostAuthoringComponentEditor.GhostDefaultOverrides`. + +You must also set the __Importance__ and __Default Client Instantiation__ property on each ghost. Unity uses the __Importance__ property to control which entities are sent when there is not enough bandwidth to send all. A higher value makes it more likely that the ghost will be sent. + +You can select from three different __Default Client Instantiation__ types: + +* __Interpolated__ - all ghosts Unity receives from the server are treated as interpolated. +* __Predicted__ - all ghosts Unity receives from the server are treated as predicted. +* __Owner predicted__ - the ghost is predicted for the client that owns it, and interpolated for all other clients. When you select this property, you must also specify which field the owner network ID is stored in. Unity compares this field to each clients’ network ID to find the correct owner. + +To override the default client instantiation you can create a partial class with the same name as the ghost spawner and implement `MarkPredictedGhosts`. + +You need to specify the paths to all generated code. You can override the defaults for this path on a per-Project basis to avoid having to manually edit all of them. For more information, see the statics in [GhostAuthoringComponentEditor](https://docs.unity3d.com/Packages/com.unity.netcode@latest/api/Unity.NetCode.Editor.GhostAuthoringComponentEditor.html), `DefaultRootPath`, `DefaultSnapshotDataPrefix`, `DefaultUpdateSystemPrefix` and `DefaultSerializerPrefix`. There is also a static to put all generated code in a namespace, `DefaultNamespace`. + +Once you have configured the ghost, select the __Generate code__ button. When a component is changed you must select both the __Update component__ list and __Generate code__ buttons to make sure Unity detects the changes and generates new code. + + +## Ghost collection + +The `GhostCollection` entity enables the ghost systems to identify the ghosts between the client and server. It contains a list of all ghosts the netcode can handle. You can use it to identify ghost types and to spawn ghosts on the client with the correct Prefab. You can also use this collection to instantiate ghosts on the server at runtime. +When the server determines which ghost type to use, it goes through the list of ghosts in the GhostCollection and picks the first one which matches. As such, the order of ghosts in the collection is important. + +In the Inspector for the __GhostCollectionAuthoringComponent__, there are three buttons you can select: +* __Update ghost list__, which scans for Prefabs with __GhostAuthoringComponent__. +* __Regenerate all ghosts__, which regenerates the code for all ghosts in the list. +* __Generate collection code__ + +For the netcode to work, you must generate the code for the collection, and the ghost collection must be part of the client and server entity worlds. + +## Value types + +The codegen does not support all value types, but you can create a new template and register it by adding a custom `GhostSnapshotValue` to `GhostSnapshotValue.GameSpecificTypes` to add support for more types. + +## Importance +The server operates on a fixed bandwidth, and sends a single packet with snapshot data of customizable size every network tick. It fills the packet with the entities of the highest importance. Several factors determine the importance of the entities: you can specify the base importance per ghost type, which Unity then scales by age. You can also supply your own method to scale the importance on a per-chunk basis. + +Once a packet is full, the server sends it and the remaining entities are missing from the snapshot. Because the age of the entity influences the importance, it is more likely that the server will include those entities in the next snapshot. The importance is only calculated per chunk, not per entity. + + +### Distance based importance +You can use a custom function to scale the importance per chunk. For example, if a singleton entity with the `GhostDistanceImportance` component on it exists on the server, the netcode makes sure that all the ghosts in the World are split into groups based on the tile size in that singleton. + +You must add a `GhostConnectionPosition` component to each connection to determine which tile the connection should prioritize. This `GhostSendSystem` passes this information to the `ScaleImportanceByDistance` in `GhostDistanceImportance` which then uses it to scale the importance of a chunk based on its distance in tiles or any other metric you define in your code. + +## Entity spawning + +When the client side receives a new ghost, a user-defined spawn system spawns it. There is no specific spawn message, and when the client receives an unknown ghost ID, it counts as an implicit spawn. Your code can generate the spawn system, along with the serializer and the logic for snapshot handling for the entity (snapshot updates). + +Because the client interpolates snapshot data, Unity cannot spawn entities immediately, unless it was preemptively spawned, such as with spawn prediction. This is because the data is not ready for the client to interpolate it. Otherwise, the object would appear and then not get any more updates until the interpolation is ready. + +Therefore normal spawns happen in a delayed manner. Spawning is split into three main types as follows: +* __Delayed or interpolated spawning.__ The entity is spawned when the interpolation system is ready to apply updates. This is how remote entities are handled, because they are interpolated in a straightforward manner. +* __Predicted spawning for the client predicted player object.__ The object is predicted so the input handling applies immediately. Therefore, it doesn't need to be delay spawned. While the snapshot data for this object arrives, the update system applies the data directly to the object and then plays back the local inputs which have happened since that time, and corrects mistakes in the prediction. +* __Predicted spawning for player spawned objects.__ These are objects that the player input spawns, like in-game bullets or rockets that the player fires. The spawn code needs to run on the client, in the client prediction system, then when the first snapshot update for the entity arrives it will apply to that predict spawned object (no new entity is created). After this, the snapshot updates are applied the same as in the predicted spawning for client predicted player object model. + +You need to implement some specific code to handle the predicted spawning for player spawned objects. Code generation handles creating some boilerplate code around a Prefab `GhostAuthoringComponent` entity definition. You can extend the entity-specific spawning by implementing a partial class of the same name as the generated spawn class. You can call the `MarkPredictedGhosts` function so that you can assign a specific ID for that type of prediction. This method must contain user defined logic to match a ghost from the server with a predicted spawned local entity. + +NetCode spawns entities on the client with a Prefab stored in the `GhostPrefabCollectionComponent` singleton. A `GhostCollectionAuthoringComponent` and a `ConvertToClientServerEntity` component that uses Client as the conversion target creates the Prefab. + +## Snapshot visualization tool + +To understand what is being put on the wire in the netcode, you can use the prototype snapshot visualization tool, __NetDbg__ in the Stats folder. To open the tool, go to menu: __Multiplayer > Open NetDbg__, and the tool opens in a browser window. It displays a vertical bar for each snapshot Unity receives, with a breakdown of the snapshot’s ghost types. To see more detailed information about the snapshot, click on one of the bars. __Note:__ This tool is a prototype. In future versions of the package it will integrate with the Unity Profiler so you can easily correlate network traffic with memory usage and CPU performance. \ No newline at end of file diff --git a/Documentation~/cube-prefab.png b/Documentation~/images/cube-prefab.png similarity index 100% rename from Documentation~/cube-prefab.png rename to Documentation~/images/cube-prefab.png diff --git a/Documentation~/final-scene.png b/Documentation~/images/final-scene.png similarity index 100% rename from Documentation~/final-scene.png rename to Documentation~/images/final-scene.png diff --git a/Documentation~/ghost-collection.png b/Documentation~/images/ghost-collection.png similarity index 100% rename from Documentation~/ghost-collection.png rename to Documentation~/images/ghost-collection.png diff --git a/Documentation~/ghost-config.png b/Documentation~/images/ghost-config.png similarity index 100% rename from Documentation~/ghost-config.png rename to Documentation~/images/ghost-config.png diff --git a/Documentation~/initial-scene.png b/Documentation~/images/initial-scene.png similarity index 100% rename from Documentation~/initial-scene.png rename to Documentation~/images/initial-scene.png diff --git a/Documentation~/mixed-world.png b/Documentation~/images/mixed-world.png similarity index 100% rename from Documentation~/mixed-world.png rename to Documentation~/images/mixed-world.png diff --git a/Documentation~/new-project.png b/Documentation~/images/new-project.png similarity index 100% rename from Documentation~/new-project.png rename to Documentation~/images/new-project.png diff --git a/Documentation~/physics-body.png b/Documentation~/images/physics-body.png similarity index 100% rename from Documentation~/physics-body.png rename to Documentation~/images/physics-body.png diff --git a/Documentation~/player-settings.png b/Documentation~/images/player-settings.png similarity index 100% rename from Documentation~/player-settings.png rename to Documentation~/images/player-settings.png diff --git a/Documentation~/images/playmode-tools.png b/Documentation~/images/playmode-tools.png new file mode 100644 index 0000000..914f5b3 Binary files /dev/null and b/Documentation~/images/playmode-tools.png differ diff --git a/Documentation~/world-game-objects.png b/Documentation~/images/world-game-objects.png similarity index 100% rename from Documentation~/world-game-objects.png rename to Documentation~/images/world-game-objects.png diff --git a/Documentation~/index.md b/Documentation~/index.md index a99de6b..2153595 100644 --- a/Documentation~/index.md +++ b/Documentation~/index.md @@ -1,165 +1,19 @@ # Unity NetCode +The Unity NetCode package provides a dedicated server model with client prediction that you can use to create multiplayer games. This documentation covers the main features of the NetCode package. -The `com.unity.netcode` package make it possible to write multiplayer games using a dedicated server model with client prediction. The main feature areas of the NetCode package are listed in this document. +**Note:** This package uses Unity’s [Entity Component System (ECS)](https://docs.unity3d.com/Packages/com.unity.entities@latest) as a foundation. As such, you must know how to use ECS to use this package. -This document is a brief description of the various parts that makes up the new netcode in Unity, it is not yet full documentation of how it works and what the APIs are since everything is still in development. +## Preview package +This package is available as a preview, so it is not ready for production use. The features and documentation in this package might change before it is verified for release. -The new NetCode for Unity is being prototyped in a small asteroids game. We deliberately chose a very simple game for prototyping since it allows us to focus on the netcode rather than the gameplay logic. The NetCode is also used in the DotsSample to get a more realistic test. +### Development status +The Unity NetCode developers are prototyping the package in a simple multidirectional shooter game, similar to Asteroids. The development team chose a very simple game to prototype with because it means they can focus on the netcode rather than the gameplay logic. The under-development DotsSample package also uses the NetCode package to get a more realistic test. -The netcode is still in a very early stage. The main focus has been on figuring out a good architecture for synchronizing entities when using ECS, there has not yet been a lot of focus on how we can make it easy to add new types of replicated entities or integrate it with the gameplay logic. These are areas we will be focusing on going forward, and areas where we want feedback. +The main focus of the NetCode development team has been to figure out a good architecture to synchronize entities when using ECS. As such, there has not yet been much exploration into how to make it easy to add new types of replicated entities or integrate it with the gameplay logic. The development team will focus on these areas going forward, and are areas where they want feedback. To give feedback on this package, post on the [Unity DOTS Forum](https://forum.unity.com/forums/data-oriented-technology-stack.147/). -## Client / Server world +## Installation +To install this package, follow the instructions in the [Package Manager documentation](https://docs.unity3d.com/Manual/upm-ui-install.html). Make sure you enable __Preview Packages__ in the Package Manager window. -The first part of the netcode is a strong separation of client and server logic into separate worlds. This is based on the new hierarchical update system in ECS. By default the NetCode package will place systems in both the client and the server world, but not in the default world. The exception to this is systems updating in the ```PresentationSystemGroup``` which will only be added to the client world. - -It is possible to override this behaviout using the ```UpdateInWorld``` attribute or by using the ```UpdateInGroup``` attribute with an explicit client/server system group. The available explicit client/server groups are ```ClientInitializationSystemGroup```, ```ServerInitializationSystemGroup```, ```ClientAndServerInitializationSystemGroup```, ```ClientSimulationSystemGroup```, ```ServerSimulationSystemGroup```, ```ClientAndServerSimulationSystemGroup``` and ```ClientPresentationSystemGroup```. Note that there is no server presentation system group. - -In addition to the attributes there is a small inspector under `Multiplayer > PlayMode Tools` which you can use to choose what should happen when entering PlayMode, you can make PlayMode client only, server only or client with in-proc server, and you can run multiple clients in the same process when entering PlayMode - all in separate worlds. In the same inspector you can disconnect clients and decide which client should be presented if you have multiple. The switching of presented client simply stops calling update on the ```ClientPresentationSystemGroup``` for the worlds which are not presented, so your game code needs to be able to handle that. -It is also possible to add thin clients instead of regular clients. Thin clients cannot be presented and they never spawn any entities received from the server, but they can still generate fake input to send to the server in order to simulate realisitc load. - -### Bootstrap - -The default bootstrap will create the client and server worlds automatically at startup and populate them with the systems defined by the attributes mentioned above. This is usually what you want to do in the editor - but when building a standalone game you sometimes want to delay the world creation so the same executable can be used as both a client and a server. -In order to do this it is possible to override the default bootstrap by creating a class extending ```ClientServerBootstrap```. You need to implement ```Initialize``` and create the default world, at a later point you can create the client and server worlds manually by calling ```ClientServerBootstrap.CreateClientWorld(defaultWorld, "WorldName");``` or ```ClientServerBootstrap.CreateServerWorld(defaultWorld, "WorldName");```. -```c# -public class ExampleBootstrap : ClientServerBootstrap -{ - public override bool Initialize(string defaultWorldName) - { - var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default); - GenerateSystemLists(systems); - - var world = new World(defaultWorldName); - World.DefaultGameObjectInjectionWorld = world; - - DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, ExplicitDefaultWorldSystems); - ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world); - return true; - } - -} -``` - -### Fixed / dynamic timestep - -When writing a game with the Unity NetCode the server will always update at a fixed timestep. The maximum number of iterations are limited to make sure it does not end up in a state where it takes several seconds to simulate a single frame. -The fixed update is not using the standard Unity update frequency, it is controlled by a singleton entity in the server world with a ```ClientServerTickRate``` component. The ```ClientServerTickRate``` component can control ```SimulationTickRate``` - number of simulation ticks per second, ```NetworkTickRate``` - number of simulation states send from the server per second, ```SimulationTickRate``` must be divisible by ```NetworkTickRate```. The default for these are 60 for both. The component also has values for ```MaxSimulationStepsPerFrame``` controlling how many simulation the server may run in a single frame and ```TargetFrameRateMode``` which controls how the server should keep the tick rate. Possible values are ```BusyWait``` for runnin at maximum speed, ```Sleep``` for used ```Application.TargetFrameRate``` to reduce CPU load or ```Auto``` to use ```Sleep``` on headless servers and ```BusyWait``` otherwise. - -The client is by default updating at a dynamic time step, with the exception of prediction code which is always running at fixed time step to match the server. The prediction runs in the ```GhostPredictionSystemGroup``` and it applies its own fixed time step specifically for prediction. It is possible to make the client use a fixed timestep by creating a singleton entity with the ```FixedClientTickRate``` component. When using fixed tick rate there are systems which can handle render interpolation if you add a ```CurrentSimulatedPosition``` and ```CurrentSimulatedRotation``` component to the entity you want to interpolate. - -## Network connection - -The network connection uses the Unity Transport package and stores each connection as an entity. Each connection entity has a ```NetworkStreamConnection``` component with the Transport handle for the connection. The connection will also have a ```NetworkStreamDisconnected``` component for one frame after disonnecting before the entity is destroyed. Disconnect can be requested by adding a ```NetworkStreamRequestDisconnect``` component to the entity, directly disconnecting using the driver is not supported. -A connection can be marked as being in-game by adding the ```NetworkStreamInGame``` component. This is never done automatically, it must be done by the game. Before this component is added to the connection sending of commands and snapshots is not enabled. -In order for commands to be stored in the correct buffer each connection also has a ```CommandTargetComponent``` which must point to the entity where the received commands should be stored. THe game is reponsible 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 an outgoing buffer for rpcs but snapshots and commands are gathered and sent in their respective send systems. When a snapshot is received it is available in the incoming snapshot buffer. The same method is used for the command stream and the RPC stream. - -When the game starts you need to manually start listening for connection on the server or conenct to a server from the client. This is not done automatically since there are not sensible defaults. To establish a connection you need to get the ```NetworkStreamReceiveSystem``` from the correct worl and call either ```Connect``` or ```Listen``` on it. - -## RPCs - -The netcode handles events by a limited form of RPC calls. The RPC calls can be issued from a job on the sending side, and they will execute in a job on the receiving side - which limits what you can do in an RPC. -In order to send an RPC you first need to get access to an RpcQueue for the command you want to send. This can be created in OnCreateManager - by calling ```m_RpcQueue = World.GetOrCreateManager().GetRpcQueue();``` - and cached through the lifetime of the game. Once you have the queue you can schedule events in it by getting the ```OutgoingRpcDataStreamBufferComponent``` buffer from the entity representing the connection you wish to send the event to and calling ```rpcQueue.Schedule(rpcBuffer, new RpcCommand);```. That will append the correct RPC data to the outgoing RPC buffer (```OutgoingRpcDataStreamBufferComponent```) so it can be sent to the remote end by ```NetworkStreamSendSystem```. -The RpcCommand interface has three methods, Serialize and Deserialize for storing the data in a packet and a CompileExecute method which should use burst to create a FunctionPointer. The function it compiles takes one parameter by ref, a struct containing: ```DataStreamReader reader, Entity connection, EntityCommandBuffer.Concurrent commandBuffer, int jobIndex``` . Since the function is static it must read the struct data using ```Deserialize``` before it can execute the RPC. The RPC can either modify the connection entity using the command buffer, or it can create a new request entity using the command buffer for more complex tasks, and apply the command in a separate system at a later time. This means that there is no need to do anything special to receive an RPC, it will have its ```Execute``` method called on the receiving end automatically. - -### RPC Command Request component - -Creating new entities which server as requests for another system to perform processing is by far the most common operation. To reduce the boilerplate for this case there is a set of helpers for doing this. You create a component extending ```IRpcCommand``` and implement the interface methods. In the execute method you compile you call ```RpcExecutor.ExecuteCreateRequestComponent(ref parameters);``` and you add ```class HeartbeatComponentRpcCommandRequestSystem : RpcCommandRequestSystem{}``` somewhere in out codebase. The goal is to generate the code for these methods in the future in a similar way to ```[GenerateAuthoringComponent]``` in entities. -Once the command request has been created you can send it to a remote end by creating and entity with the specific ```IRpcCommand``` and a ```SendRpcCommandRequestComponent``` with the target connection entity. If the target connection entity is ```Entity.Null``` the request will be sent to all connections. The system will automatically find these request, send them and delete the send request. On the remote side they will show up as entities with the same ```IRpcCommand``` and a ```ReceiveRpcCommandRequestComponent``` which can be used to identify which connection the request was received from. - -## Command stream - -The client will continuously send a command stream to the server. This stream includes all inputs and acknowledgements of the last received snapshot. When no commands are being sent a ```NullCommandSendSystem``` sends acks for received snapshots without any inputs. This is an automatic system to make sure the flow works automatically when the game does not need to send any inputs. - -To create a new input type you must create a struct implementing the ```ICommandData``` interface. In order to implement that interface you must provide methods for accessing the ```Tick``` as well as ```Serialize``` and ```Deserialize```. There are two variations of serialize and deserialize, one which can delta compress and one which sends raw values. The system sends multiple inputs in each command packet, the first one is raw data but the rest are compressed using delta compression. Inputs tend to compress very well using delta compression since the rate of change is low. - -In addition to creating a struct you need to create specific instances of the generic systems ```CommandSendSystem``` and ```CommandReceiveSystem```. You can do that by extending the base system, for example ```class MyCommandSendSystem : CommandSendSystem{}```. - -Just having the input buffer on an entity is not enough for the netcode to start using it. The game code must also set the ```CommandTargetComponent``` on the connection entity to reference the entity to which the ```ICommandData``` component has been attached. - -It is possible to have multiple command systems, the netcode will pick the correct one based on which ```ICommandData``` type the entity pointed to be ```CommandTargetComponent``` has. - -When accessing inputs on the client and server it is important to read the data from the ```ICommandData``` rather than reading it directly from the system. If the data is read directly from the system the inputs will not match between the client and server so the game will not behave as expected. When accessing the inputs from the buffer there is an extension method for ```DynamicBuffer``` called ```GetDataAtTick``` which can be used to get the matching tick for a specific frame. There is also an ```AddCommandData``` utility method which should be used to add more commands to the buffer. - -## Ghost snapshots - -The ghost snapshot system is the most complex part of the netcode. It is responsible for synchronizing entities which exist on the server to all clients. In order to make it perform well the server will do all processing per ECS chunk rather than per entity. On the receiving side the processing is done per entity. The reason for this is that it is not possible to process per chunk on both sides, and the server has more connections than clients. - -### Ghost authoring component -The system is based on specifying ghosts as prefabs with the ```GhostAuthoringComponent``` on them. The ```GhostAuthoringComponent``` has a small editor which is used to configure how the various components and fields in the prefab will be synchronized as well as where the components will be available. - -When a ```GhostAuthoringComponent``` has been added to the prefab you need to click the "Update component list" button in the inspector. This will determine which entities the prefab will have after conversion and add them to the component list. - -The list is used to configure the ghost. There are checkboxes which are used to select which version of the prefab will have a component. If you for example uncheck "server" from "RenderMesh" the ghost will not have a RenderMesh when it is instantiated on the server, but it will have it when instantiated on the client. The defaults for these checkboxes can be controlled from source by adding the ```GhostDefaultComponentAttribute``` to an IComponentData. - -For each component you need to decide which values to synchronize and which quality they need. You can setup defaults for the values by adding the ```GhostDefaultFieldAttribute``` attribute to the fields in an IComponentData. -The default values for synchronization can be overridden for a specific ghost by checking the "Manual Field List" checkbox and editing them. - -In addition to adding attributes it is possible to override defaults for components which you do not have source access to by creating an entry to ```GhostAuthoringComponentEditor.GhostDefaultOverrides```. - -There are a few more parameters which need to be setup on each ghost. The importance and default client instantiation type. The importance is used to control which entities are sent when there is not enough bandwidth to send all, higher value makes it more likely that the ghost will be sent. -The default client instantiation type can be `Interpolated` which means all ghosts received from the server will be treated as interpolated, `Predicted` which means all ghost received will be treated as prediced, or `Owner predicted` which means the ghost will be predicted for the client that owns it and interpolated for all otehr clients. When selecting `Owner predicted` you must also specify which field the owner network id is stored in. This field is compared to each clients network id to find the correct owner. -The default client instantiation is just the default. It is possible to override it by creating a partial class with the same name as the ghost spawner and implement ```MarkPredictedGhosts```. - -The paths to all generated code needs to be specified. It is possible to override the defaults for this path on a per project basis to avoid having to manually edit all of them, see the statics in ```GhostAuthoringComponentEditor```, ```DefaultRootPath```, ```DefaultSnapshotDataPrefix```, ```DefaultUpdateSystemPrefix``` and ```DefaultSerializerPrefix```. There is also a static to put all generated code in a namespace, ```DefaultNamespace```. - -Once everything is configured you must click the generate code button. When a component is changed you must also click both these buttons to make sure the changes are detected and new code is generated. - -### Ghost collection - -In order for the ghost systems to have a way of identifying the ghosts between the client and server there must be a `GhostCollection`. A GhostCollection is an entity containing a list of all ghosts the netcode can handle. It is used for identifying ghost types and also to spawn ghosts on the client with the correct prefab. It can also be used to spawn ghosts on the server in a programatic way. - -When the server determinies which ghost type to use for a ghost it goes through the list of ghosts in the GhostCollection and picks the first one which matches. This means that the order of ghosts in the collection is important. - -The inspector for ```GhostCollectionAuthoringComponent``` has buttons to scan for prefabs with ```GhostAuthoringComponent```, regenerate the code for all ghosts in the list and generate the code for the collection. - -For the netcode to work the code for the collection must be generated and the ghost collection must be part of the cient and server entity worlds. - -### Value types - -The codegen does not support all value types, but it is possible to add support for more types by creating a new template and registering it by adding a custom ```GhostSnapshotValue``` to ```GhostSnapshotValue.GameSpecificTypes```. - -### Importance -The server operates on a fixed bandwidth, sending a single packet with snapshot data of customizable size every network tick. The packet is filled with the entities of the highest importance. Once a packet is full it is sent and remaining entities will be missing from the snapshot. Since the age influences the importance it is more likely that those entities will be included in the next snapshot. The importance is only calculated per chunk, not per entity. - -#### Distance based importance -The importance per chunk can be scaled by a custom user defined function. If a singleton entity with the ```GhostDistanceImportance``` component on it exists on the server the netcode will make sure that all ghosts in the world are split into groups based on the tiles they are in defined by the tile size in that singleton. Each connection must have a ```GhostConnectionPosition``` component added to them to determine which tile the connection should prioritize. -This information is passed to the ```ScaleImportanceByDistance``` in ```GhostDistanceImportance``` which can use it to scale the importance of a chunk based on distance in tiles or some other metric. - -### Entity spawning - -When a new ghost is received on the client side it will be spawned by a user defined spawn system. There is no specific spawn message, receiving an unknown ghost id counts as an implicit spawn. The spawn system can be code generated along with the serializer and the logic for snapshot handling in general for he entity (snapshot updates). - -Because of how snapshot data is interpolated the entity spawning/creation needs to be handled in a special manner. The entity can't be spawned immediately unless it was preemptively spawned (like with spawn prediction), since the data is not ready to be interpolated yet. Otherwise the object would appear and then not get any more updates until the interpolation is ready. Therefore normal spawns happen in a delayed manner. Spawning can be split into 3 main types. - -1. Delayed or interpolated spawning. Entity is spawned when the interpolation system is ready to apply updates. This is how remote entities are handled as they are being interpolated in a straightforward manner. -2. Predicted spawning for the client predicted player object. The object is being predicted so input handling applies immediately, therefore it doesn't need to be delay spawned. As snapshot data for this object arrives the update system handles applying the data directly to the object and then playing back the local inputs which have happened since that time (correcting mistakes in prediction). -3. Predicted spawning for player spawned objects. These are objects spawned from player input, like bullets or rockets being fired by the player. The spawn code needs to run on the client, in the client prediction system, then when the first snapshot update for the entity arrives it will apply to that predict spawned object (no new entity is created). After this the snapshot updates are applied just like in case 2. - -Handling the third case of predicted spawning requires some user code to be implemented to handle it. Code generation will handle creating some boilerplate code around a prefab ```GhostAuthoringComponent``` entity definition. The entity specific spawning can be extended by implementing a partial class of the same name as the generated spawn class. A function called ```MarkPredictedGhosts``` is called so you can assign a specific ID for that type of prediction. This method must contain user defined logic to match a ghost from the server with a predicted spawned local entity. - -Eentities on the client are spawned with a prefab stored in the ```GhostPrefabCollectionComponent``` singleton. The prefab is created by having a ```GhostCollectionAuthoringComponent``` and a ```ConvertToClientServerEntity``` component using Client as the conversion target. - -### Snapshot visualization - -In order to reason about what is being put on the wire in the netcode we have a small prototype of a visualization tool in the Stats folder. The tool will display one vertical bar for each received snapshot with breakdown of ghost types in that snapshot. Clicking a bar will display more detailed stats about the snapshot. This tool is a prototype, in the future it will be integrated with the unity profiler to make it easier to correlate network traffic with memory usage and CPU performance. -The tool can be opened from the `Multiplayer > Open NetDbg` menu. - -## Time synchronization - -Since the netcode is using a server authoritative model the server is executing a fixed time step based on how much time has passed, but the client needs to match the server time at all time for the model to work. - -The client should present the tick the server will simulate right after the commands the client sends it now arrives, which has not yet happened. This is called the prediction time. -Calculating which server time to present on the client is handled by the ```NetworkTimeSystem```. The network time system will calculate an initial estimate of server time based on round trip time and latest received snapshot from the server. -Once the client has an initial estimate it will try to adjust to changes be making time progress slightly faster or slower rather than doing big changes to current time. -In order to make acurate adjustments the server will track how long before they are used the commands arrive. This is sent back to the client and the client tries to adjust its time in a way that commands arrive a little bit before they are required. - -The prediction time - as the name implices - should only be used for predicted object like the local player. For interpolated objects the client should present them in a state it has received data for. -This time, called interpolation time, is calculated as a time offset from the prediction time. The offset is called prediction delay and it is slowly adjusted up and down in small increments to keep the interpolation time advancing at a smooth rate. -The interpolation delay is calculated from round trip time and jitter in a way that the data is generally available. The delay also adds additional time based on the network tick rate to make sure it can handle a packet being lost. - -The time offsets and scales discussed in this section can be visualized as graphs in NetDbg. - -# Getting started - -If you just want to get started making a multiplayer game the [quickstart](quickstart.md) will walk you through how to setup a simple multiplayer enabled project. +## Requirements +This version of Unity NetCode is compatible with the following versions of the Unity Editor: +* 2019.3b11 and later (recommended) diff --git a/Documentation~/network-connection.md b/Documentation~/network-connection.md new file mode 100644 index 0000000..b50db20 --- /dev/null +++ b/Documentation~/network-connection.md @@ -0,0 +1,11 @@ +# Network connection + +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. + +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. + +To store commands in the correct buffer, 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. + +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. + +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. diff --git a/Documentation~/prediction.md b/Documentation~/prediction.md new file mode 100644 index 0000000..f212d3e --- /dev/null +++ b/Documentation~/prediction.md @@ -0,0 +1,23 @@ +# Prediction + +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. + +The prediction is based on a [GhostPredictionSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@0latest/index.html?subfolder=/api/Unity.NetCode.GhostPredictionSystemGroup.html) which always runs at a fixed timestep to get the same results on the client and server. + +## 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 [GhostPredictionSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostPredictionSystemGroup.html) runs from the oldest tick applied to any entity, to the tick the prediction is targeting. +* When the prediction runs, the `GhostPredictionSystemGroup` sets the correct time for the current prediction tick in the ECS TimeData struct. It also sets [GhostPredictionSystemGroup.PredictingTick](https://docs.unity3d.com/Packages/com.unity.netcode@lates/index.html?subfolder=/api/Unity.NetCode.GhostPredictionSystemGroup.html#Unity_NetCode_GhostPredictionSystemGroup_PredictingTick) 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, call the static method [GhostPredictionSystemGroup.ShouldPredict](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostPredictionSystemGroup.html#Unity_NetCode_GhostPredictionSystemGroup_ShouldPredict_System_UInt32_Unity_NetCode_PredictedGhostComponent_) before updating an entity. If it returns `false` the update should not run for that entity. + +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. It still sets `GhostPredictionSystemGroup.PredictingTick` to make sure the exact same code can be run on both the client and server. diff --git a/Documentation~/rpcs.md b/Documentation~/rpcs.md new file mode 100644 index 0000000..2c256bc --- /dev/null +++ b/Documentation~/rpcs.md @@ -0,0 +1,28 @@ +# RPCs + +NetCode uses a limited form of RPC calls to handle events. A job on the sending side can issue RPC calls, and they then execute on a job on the receiving side. This limits what you can do in an RPC. + +To send an RPC, you need to get access to an [RpcQueue](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.RpcQueue-1.html) of the command you want to send. You can create this in OnCreateManager. Call `m_RpcQueue = World.GetOrCreateManager().GetRpcQueue();` and cache it through the lifetime of the game. When you have the queue, you can get the `OutgoingRpcDataStreamBufferComponent` from an entity to schedule events in the queue and then call `rpcQueue.Schedule(rpcBuffer, new RpcCommand);`. + +You can send an RPC from OnUpdate as follows: +* Get the `OutgoingRpcDataStreamBufferComponent` from the entity you want to send the event to. +* Call `rpcQueue.Schedule(rpcBuffer, new RpcCommand);` to append the RPC data you want to sent to the outgoing RPC buffer (`OutgoingRpcDataStreamBufferComponent`). +* Once this is done, the `NetworkStreamSendSystem` sends the queued RPC to the remote end. + +The RpcCommand interface has three methods: __Serialize, Deserialize__, and __CompileExecute__. __Serialize__ and __Deserialize__ store the data in a packet, while __CompileExecute__ uses Burst to create a `FunctionPointer`. The function it compiles takes one parameter by ref, and a struct that contains: + +* `DataStreamReader` reader +* Entity connection +* `EntityCommandBuffer.Concurrent` commandBuffer +* int `jobIndex` + +Because the function is static, it needs to use `Deserialize` to read the struct data before it can execute the RPC. The RPC can then either use the command buffer to modify the connection entity, or use it to create a new request entity for more complex tasks and then apply the command in a separate system at a later time. This means that you don’t need to perform any additional operations to receive and RPC; its `Execute` method is called on the receiving end automatically. + +## RPC command request component +Most RPCS create new entities which serve as requests for other systems to perform an operation. To reduce the boilerplate for this, NetCode has a set of helpers that perform these operations. To do this perform the following steps: + +* Create a component that extends `IRpcCommand` and implement the interface methods. +* In the execute method, call `RpcExecutor.ExecuteCreateRequestComponent(ref parameters);` and add class `HeartbeatComponentRpcCommandRequestSystem : RpcCommandRequestSystem{}` to the codebase. **Note:** In future, the NetCode development team will provide a way to generate code for these methods in a similar way to `GenerateAuthoringComponent` in entities. +* Once you have created a command request, use `IRpcCommand` and `SendRpcCommandRequestComponent` with the target connection entity to send it to a remote end. If the target connection entity is `Entity.Null`, Unity sends it to all connections. + +The system automatically finds the requests, sends them, and then deletes the send request. On the remote side they show up as entities with the same `IRpcCommand` and a `ReceiveRpcCommandRequestComponent` which you can use to identify which connection the request was received from. \ No newline at end of file diff --git a/Documentation~/time-synchronization.md b/Documentation~/time-synchronization.md new file mode 100644 index 0000000..a5baabf --- /dev/null +++ b/Documentation~/time-synchronization.md @@ -0,0 +1,9 @@ +## 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. + +[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 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. + +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). \ No newline at end of file diff --git a/Runtime/Connection/NetworkStreamReceiveSystem.cs b/Runtime/Connection/NetworkStreamReceiveSystem.cs index 3b9eced..49c5b71 100644 --- a/Runtime/Connection/NetworkStreamReceiveSystem.cs +++ b/Runtime/Connection/NetworkStreamReceiveSystem.cs @@ -35,10 +35,10 @@ public class NetworkStreamReceiveSystem : JobComponentSystem private NativeQueue freeNetworkIds; private BeginSimulationEntityCommandBufferSystem m_Barrier; private RpcQueue rpcQueue; - private int m_ClientPacketDelay; - private int m_ClientPacketDrop; private EntityQuery m_NetworkStreamConnectionQuery; #if UNITY_EDITOR || DEVELOPMENT_BUILD + private int m_ClientPacketDelay; + private int m_ClientPacketDrop; private NativeArray m_NetStats; private GhostStatsCollectionSystem m_GhostStatsCollectionSystem; #endif @@ -66,19 +66,23 @@ public Entity Connect(NetworkEndPoint endpoint) LastDriverWriter.Complete(); if (m_UnreliablePipeline == NetworkPipeline.Null) { +#if UNITY_EDITOR || DEVELOPMENT_BUILD if (m_ClientPacketDelay > 0 || m_ClientPacketDrop > 0) m_UnreliablePipeline = m_Driver.CreatePipeline(typeof(SimulatorPipelineStage), typeof(SimulatorPipelineStageInSend)); else +#endif m_UnreliablePipeline = m_Driver.CreatePipeline(typeof(NullPipelineStage)); } if (m_ReliablePipeline == NetworkPipeline.Null) { +#if UNITY_EDITOR || DEVELOPMENT_BUILD if (m_ClientPacketDelay > 0 || m_ClientPacketDrop > 0) m_ReliablePipeline = m_Driver.CreatePipeline(typeof(SimulatorPipelineStageInSend), typeof(ReliableSequencedPipelineStage), typeof(SimulatorPipelineStage)); else +#endif m_ReliablePipeline = m_Driver.CreatePipeline(typeof(ReliableSequencedPipelineStage)); } diff --git a/Runtime/RenderInterpolation/AfterSimulationInterpolationSystem.cs b/Runtime/RenderInterpolation/AfterSimulationInterpolationSystem.cs index aaa5c49..7713bf9 100644 --- a/Runtime/RenderInterpolation/AfterSimulationInterpolationSystem.cs +++ b/Runtime/RenderInterpolation/AfterSimulationInterpolationSystem.cs @@ -22,21 +22,21 @@ public class AfterSimulationInterpolationSystem : JobComponentSystem protected override void OnCreate() { - positionInterpolationGroup = GetEntityQuery(ComponentType.ReadOnly(), - ComponentType.ReadOnly(), ComponentType.ReadWrite()); - rotationInterpolationGroup = GetEntityQuery(ComponentType.ReadOnly(), - ComponentType.ReadOnly(), ComponentType.ReadWrite()); + positionInterpolationGroup = GetEntityQuery(ComponentType.ReadWrite(), + ComponentType.ReadOnly(), ComponentType.ReadOnly()); + rotationInterpolationGroup = GetEntityQuery(ComponentType.ReadWrite(), + ComponentType.ReadOnly(), ComponentType.ReadOnly()); newPositionInterpolationGroup = GetEntityQuery(new EntityQueryDesc { All = new[] { - ComponentType.ReadWrite(), ComponentType.ReadOnly() + ComponentType.ReadOnly(), ComponentType.ReadWrite() }, None = new[] {ComponentType.ReadWrite()} }); newRotationInterpolationGroup = GetEntityQuery(new EntityQueryDesc { - All = new[] {ComponentType.ReadWrite(), ComponentType.ReadOnly()}, + All = new[] {ComponentType.ReadOnly(), ComponentType.ReadWrite()}, None = new[] {ComponentType.ReadWrite()} }); diff --git a/Runtime/Snapshot/GhostSendSystem.cs b/Runtime/Snapshot/GhostSendSystem.cs index 8c95610..7afba6e 100644 --- a/Runtime/Snapshot/GhostSendSystem.cs +++ b/Runtime/Snapshot/GhostSendSystem.cs @@ -753,6 +753,7 @@ protected override JobHandle OnUpdate(JobHandle inputDeps) } m_ConnectionStates.RemoveAt(m_ConnectionStates.Count - 1); + --i; } } diff --git a/package.json b/package.json index 888d7fc..9850716 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { "name": "com.unity.netcode", "displayName": "Unity NetCode", - "version": "0.0.2-preview.1", + "version": "0.0.3-preview.2", "unity": "2019.3", "unityRelease": "0b11", "description": "Unity multiplayer netcode layer - a high level netcode system built on entities", "dependencies": { - "com.unity.transport": "0.2.1-preview.1", - "com.unity.entities": "0.2.0-preview.18" + "com.unity.transport": "0.2.2-preview.2", + "com.unity.entities": "0.3.0-preview.4" }, "repository": { "type": "git", "url": "git@github.com:Unity-Technologies/netcode.git", - "revision": "dc6ff1b90bd08de36cbf05fc07d05cc47f6ba114" + "revision": "a66f6695feb879621aa4a464939ffb6d92d3be4e" } }