diff --git a/.buginfo b/.buginfo new file mode 100644 index 0000000..6fa36b8 --- /dev/null +++ b/.buginfo @@ -0,0 +1,4 @@ +system: jira +server: jira.unity3d.com +project: ECSB +issuetype: Bug \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a36e9a6..d6c3a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,68 @@ # Changelog -## [1.0.0-exp.13] - 2022-10-19 +## [1.0.0-pre.15] - 2022-11-16 + +### Added + +* A "Client & Server Bounding Boxes" debug drawer has been added to the package (at `Packages\com.unity.netcode\Editor\Drawers\BoundingBoxDebugGhostDrawerSystem.cs`), allowing you to view the absolute positions of where the client _thinks_ a Ghost is, vs where it _actually_ is on the server. This drawer can also be used to visualize relevancy logic (as you can see widgets for server ghosts that are "not relevant" for your client). Enable & disable it via the `Multiplayer PlayMode Tools Window`. +* FRONTEND_PLAYER_BUILD scripting define added to the NetCodeClientSetting project setting. +* new GhostSpawnBufferInspectorHelper and GhostSpawnBufferComponentInspector structs, that permit to read from the ghost spawn buffer any type of component. They can be used in spawn classification systems to help resolving predicted spawning requests. +* GhostTypeComponent explicit conversion to Hash128. +* Templates for serialising `double` type. +* A `TransformDefaultVariantSystem` that optionally setup the default variant to use for `LocalTransform`, (`Rotation`, `Position` for V1) if a user defined default is not provided. +* A `PhysicsDefaultVariantSystem` that optionally setup the default variant to use for `PhysicVelocity` if a user defined default is not provided. + +### Changed + +* Updated com.unity.transport dependency to 2.0.0-exp.4 +* SharedGhostTypeComponent is also added to the client ghost prefab to split ghosts in different chunks. +* GhostTypeComponent equals/match the prefab guid. +* Removed `CodeGenTypeMetaData`, and made internal changes to how `VariantType` structs are generated. We also renamed `VariantType` to `ComponentTypeSerializationStrategies` to better reflect their purpose, and to better distinguish them from the concept of "Variants". +* Support for replicating "enable bits" on `IEnableableComponent`s (i.e. "enableable components") via new attribute `GhostEnabledBitAttribute` (`[GhostEnabledBit]`), which can be added to the component struct. +Note: If this attribute is **not** added, your enabled bits will not replicate (even on components marked with `[GhostField]`s). +_This is a breaking change. Ensure all your "enableable components" with "ghost fields" on them now also have `[GhostEnabledBit]` on the struct declaration._ +* All `DefaultVariantSystemBase` are all grouped into the `DefaultVariantSystemGroup`. +* It is not necessary anymore to define a custom `DefaultGhostVariant` system if a `LocalTransform` (`Rotation` or `Position` for V1) or `PhysicsVelocity` variants are added to project (since a default selection is already provided by the package). +* Updated com.unity.transport dependency to 2.0.0-pre.2 + + +### Removed + +* Removing dependencies on `com.unity.jobs` package. ### Fixed +* Error in source generator when input buffer type was in default namespace +* Always pass SystemState by ref to avoid UnsafeLists being reallocated in a copy but not the original. +* Use correct datatype for prespawned count in analytics. +* Use EditorAnalytics to verify whether it is enabled. +* exception thrown by the hierarchy window if a scene entity does not have a SubScene component. +* Issue with the GhostComponentSerializerRegistrationSystem and the ghost metadata registration system trying accessing the GhostComponentSerializerCollectionData before it is created. +* A crash in the GhostUpdateSystem, GhostPredictionHistorySystem and others, when different ghost types (prefab) share/have the same archetype. +* A NetCodeSample project issue that was causing screen flickering and entities rendered multiple times when demos were launched from the Frontend scene. +* an issue in the GhostSendSystem that prevent the DataStream to be aborted when an exception is throw while serialising the entities. +* InvalidOperationException in the `GhostAuthoringInspectionComponent` when reverting a Variant back to the default. +* UI layout issues with the `GhostAuthoringInspectionComponent`. +* Hashing issue with `GhostAuthoringInspectionComponent.ComponentOverrides` during baking, where out-of-date hashes would still be baked into the BlobAsset. You now get an error, pointing you to the offending (i.e. out-of-date) Ghost prefab. * quaternion cannot be added as field in ICommandData and/or IInputComponentData. A new region has been added to the code-generation templates for handling similar other cases. -* Removed the deprecated NativeList to NativeArray implicit cast and use NativeList.AsArray instead. -* fixed a NotImplementedException thrown in standalone player client build. +* Fixed hash generation when using `DontSerializeVariant` (or `ClientOnlyVariant`) on `DefaultVariantSystemBase.Rule`. They now use the constant hashes (`DontSerializeHash` and `ClientOnlyHash`). +* NetDbg will now correctly show long namespaces in the "Prediction Errors" section (also: improved readability). +* Removed CSS warning in package. +* A problem with baking and additional ghost entities that was removing `LocalTransform`, `WorldTransform` and `LocalToWorld` matrix. +* Mismatched ClientServerTickRate.SimulationTickRate and PredictedFixedStepSimulationSystemGroup.RateManager.Timestep will throw an error and will set the values to match each other. +### Upgrade Guide +* `NetCodeClientTarget` moved from namespace `Authoring.Hybrid` to `Unity.NetCode.Hybrid` +* `VariantType` has been renamed to `SerializationStrategy`, in an attempt to disambiguate the roles of “Variants” vs “Serialization Strategies”. + * `ComponentTypeSerializationStrategy` - Denotes the rules used to serialize a specific Component, on a specific GhostType. + * “Variant” (via `GhostComponentVariation`) - The name for a custom struct modifying the serialization rules for a single `ComponentType`. +* Defining `DefaultVariantSystemBase` rules has changed. You now must pass in a `DefaultVariantSystemBase.Rule` rather than the `System.Type` of the VariantType. +* `VariantHash`es for ghost variants may have changed. Make sure that there are no errors when baking all GhostTypes (in subscenes and prefabs), and use the `GhostAuthoringInspectionComponent` to debug and validate that the “default rules” (for your `ComponentTypes`) are correct. Note that GhostFields on components on child entities are no longer replicated (i.e. serialized) by default. +* TRANSFORMS_V2 will break all Variant modifications made to (now legacy) `Translation` and `Rotation` Components. You'll need to either: + * a) reimplement these custom Variants yourself (targeting `LocalTransform`) or + * b) use one of the new built-in `LocalTransform` Variants provided. + * Note that currently, these invalid "Component Overrides" on `GhostAuthoringInspectionComponents` are not automatically updated for you. They are simply force-deleted (with an associated error message, which should provide some useful context). @@ -100,9 +154,6 @@ * DefaultUserParams has been renamed to DefaultSmoothingActionUserParams. * DefaultTranslateSmoothingAction has been renamed to DefaultTranslationSmoothingAction. -### Deprecated - - ### Removed * The static bool `RpcSystem.DynamicAssemblyList` has been removed, replaced by a non-static property with the same name. See upgrade guide (below). @@ -126,9 +177,6 @@ * Ensure that we do not count zero update length in analytic results. Fix assertion error when entering and exiting playmode * Compilation errors when the DedicatedServer platform is selected. NOTE: this does not imply the dedicated server platform is supported by the NetCode package or any other packages dependencies. -### Security - - ### Upgrade guide * Prefer using the new unified `NetCodePhysicsConfig` authoring component instead of using the `LagCompensationConfig` authoring component to enable lag compensation. diff --git a/Documentation~/TableOfContents.md b/Documentation~/TableOfContents.md index 497d3f9..9dbbe71 100644 --- a/Documentation~/TableOfContents.md +++ b/Documentation~/TableOfContents.md @@ -1,14 +1,23 @@ -* [About Unity NetCode](index.md) +* [About Netcode for Entities](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) -* [Optimizations](optimizations.md) -* [Logging](logging.md) -* [Physics](physics.md) -* [Ghost types and variants](ghost-types-templates.md) -* [List of entities](entities-list.md) + * [Installation](installation.md) + * [Networked Cube](networked-cube.md) +* [Basics](basics.md) + * [Networking Model](client-server-worlds.md) + * [Connection](network-connection.md) + * [Communication](rpcs.md) + * [Synchronization](synchronization.md) + * [Ghost Synchronization](ghost-snapshots.md) + * [Ghost Spawning](ghost-spawning.md) + * [Commands](command-stream.md) + * [Time](time-synchronization.md) + * [Netcode specific Components and Types](entities-list.md) +* [Advanced](advanced.md) + * [Ghost Type Templates](ghost-types-templates.md) + * [Physics](physics.md) + * [Prediction](prediction.md) + * [Optimizations](optimizations.md) +* [Debugging and Tools](debugging.md) + * [Playmode-Tool](playmode-tool.md) + * [Logging](logging.md) + * [Metrics](metrics.md) diff --git a/Documentation~/advanced.md b/Documentation~/advanced.md new file mode 100644 index 0000000..677b57a --- /dev/null +++ b/Documentation~/advanced.md @@ -0,0 +1,8 @@ +# Advanced Topics + +| **Topic** | **Description** | +| :-------------------- | :----------------------- | +| **[Ghost Type Templates](ghost-types-templates.md)** | Templates in Netcode for Entities | +| **[Physics](physics.md)** | Physics synchronization in Netcode for Entities | +| **[Prediction](prediction.md)** | Prediction synchronization in Netcode for Entities | +| **[Optimizations](optimizations.md)** | Optimizations in Netcode for Entities | diff --git a/Documentation~/basics.md b/Documentation~/basics.md new file mode 100644 index 0000000..0eebc59 --- /dev/null +++ b/Documentation~/basics.md @@ -0,0 +1,16 @@ +# Netcode for Entities Basics + +The Netcode for Entities 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 for Entities package. + +| **Topic** | **Description** | +| :-------------------- | :----------------------- | +| **[Networking Model](client-server-worlds.md)** | Describes the overall networking model used in Netcode for Entities | +| **[Connection](network-connection.md)** | Describes the connection model used in Netcode for Entities | +| **[Communication](rpcs.md)** | Describes the communication model used in Netcode for Entities | +| **[Synchronization](synchronization.md)** | Describes the synchronization model used in Netcode for Entities | +| **[Time](time-synchronization.md)** | Describes the timing model used in Netcode for Entities | +| **[Netcode specific Components and Types](entities-list.md)** | Describes the Components and Types used in Netcode for Entities | + + +## Additional resources +- [Advanced Topics](advanced.md) diff --git a/Documentation~/client-server-worlds.md b/Documentation~/client-server-worlds.md index 425e105..8a277cf 100644 --- a/Documentation~/client-server-worlds.md +++ b/Documentation~/client-server-worlds.md @@ -1,44 +1,59 @@ # 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. +The Netcode for Entities Package has a separation between client and server logic, and thus, splits logic into multiple Worlds (the "Client World", and the "Server World"). +It does this using concepts laid out in the [hierarchical update system](https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-update-order.html) of Unity’s Entity Component System (ECS). + +## Declaring in which world the system should update. +By default, systems are create into (and updated in) the `SimulationSystemGroup`, and created for both client and server worlds. In cases where you want to override that behaviour (i.e. have your system +created and run only on the client world), you have two different way to do it: + +### Targeting specific system groups +By specifying that your system belongs in a specific system group (that is present only on the desired world), your system will automatically **not** be created in worlds where this system group is not present. +In other words: Systems in a system group inherit system group world filtering. For example: +```csharp +[UpdateInGroup(typeof(GhostInputSystemGroup))] +public class MyInputSystem : SystemBase +{ + ... +} +``` +Because the `GhostInputSystemGroup` exists only for Client worlds, the `MyInputSystem` will **only** be present on the client world (caveat: this includes both `Client` and `Thin Client` worlds). > [!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) +> Systems that update in the `PresentationSystemGroup` are only added to the client World, since the `PresentationSystemGroup` is not created for `Server` and `Thin Client` worlds. -> [!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__. +### Use WorldSystemFilter +When more granularity is necessary (or you just want to be more explicit about which World type(s) the system belongs to), you should use the +[WorldSystemFilter](https://docs.unity3d.com/Packages/com.unity.entities@latest/index.html?subfolder=/api/Unity.Entities.WorldSystemFilter.html) attribute. -![PlayMode Tools](images/playmode-tools.png)
_PlayMode Tools_ +Context: When an entity `World` is created, users tag it with specific [WorldFlags](https://docs.unity3d.com/Packages/com.unity.entities@latest/index.html?subfolder=/api/Unity.Entities.WorldFlags.html), +that can then be used by the Entities package to distinguish them (e.g. to apply filtering and update logic). -|**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.| -|__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 discards 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. | +By using the `WorldSystemFilter`, you can declare (at compile time) which world types your system belongs to: +- `LocalSimulation`: a world that does not run any Netcode systems, and that it is not used to run the multiplayer simulation. +- `ServerSimulation`: A world used to run the server simulation. +- `ClientSimulation`: A world used to run the client simulation. +- `ThinClientSimulation`: A world used to run the thin clients simulation. -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. +```csharp +[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] +public class MySystem : SystemBase +{ + ... +} +``` +In the example above, we declared that the `MySystem` system should **only** be present for worlds that can be used for running the `client simulation`; That it, the world has the `WorldFlags.GameClient` set. +`WorldSystemFilterFlags.Default` is used when this attribute is not present. ## Bootstrap +When the Netcode for Entities package is added to your project, a new default [bootstrap](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerBootstrap.html) is added to the project. -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. +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 and you enter play-mode with your game scene opened. +But in a standalone game, or when you want to use some sort of frontend menu, you might want to delay the World creation, i.e 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");`. +It it possible to create your own bootstrap class and customise your game flow by creating a class that extends `ClientServerBootstrap` and override the default `Initialize` method implementation. +You can re-use in your class mostly of the provided helper methods that can let you create `client`, `server`, `thin-client` and `local simulation` worlds. See for more details [ClientServerBootstrap methods](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerBootstrap.html). The following code example shows how to override the default bootstrap to prevent automatic creation of the client server worlds: @@ -47,45 +62,72 @@ 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.AppendWorldToCurrentPlayerLoop(world); + //Create only a local simulation world without any multiplayer and netcode system in it. + CreateLocalWorld(defaultWorldName); return true; } } ``` -## Fixed and dynamic timestep +## Fixed and dynamic time-step -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. +When you use Netcode for Entities, the server always updates **at a fixed time-step**. The package also limits the maximum number of fixed-step iterations per frame, 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. +It is therefore important to understand that the fixed update does not use the [standard Unity update frequency](https://docs.unity3d.com/Manual/class-TimeManager.html). -> [!NOTE] -> `SimulationTickRate` must be divisible by `NetworkTickRate`. +### Configuring the Server fixed update loop. +The [ClientServerTickRate](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerTickRate.html) singleton component (in the server World) controls this tick-rate. + +By using the `ClientServerTickRate`, you can control different aspects of the server simulation loop. For example: +- The `SimulationTickRate` lets you configure the number of simulation ticks per second. +- The `NetworkTickRate` lets you configure how frequently the server sends snapshots to the clients (by default the `NetworkTickRate` is identical to the `SimulationTickRate`). + +**The default number of simulation ticks is 60**. -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: +If the server updates at a lower rate than the simulation tick rate, it will perform multiple ticks in the same frame. For example, if the last server update took 50ms (instead of the usual 16ms), the server will need to `catch-up`, and thus it will do ~3 simulation steps on the next frame (16ms * 3 ≈ 50ms). + +This behaviour can lead to what is known as `the spiral of death`; the server update becomes slower and slower (because it is executing more steps per update, to catch up), thus, ironically, putting it further behind (creating more problems). +The `ClientServerTickRate` allows you to customise how the server runs in this particular situation (i.e. when the server cannot maintain the desired tick-rate). + +By setting the [MaxSimulationStepsPerFrame](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerTickRate.html#ClientServerTickRate_MaxSimulationStepsPerFrame) +you can control how many simulation steps the server can run in a single frame.
+By using the [MaxSimulationStepBatchSize](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerTickRate.html#MaxSimulationStepBatchSize) +you can instruct the server loop to `batch` together multiple ticks into a single step, but with a multiplier on the delta time. For example, instead of running two step, you can run only one (but with double the delta time). +> [!NOTE] +> This batching only works under specific conditions, and has its own nuances and considerations. Ensure that your game does not make any assumptions that one simulation step is "1 tick" (nor should you hardcode deltaTime). +Finally, you can configure how the server should consume the the idle time to target the desired frame rate. +The [TargetFrameRateMode](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientServerTickRate.html#TargetFrameRateMode) 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 -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 [PredictedSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedSimulationSystemGroup.html) and applies its own fixed time step for prediction. + +### Configuring the Client update loop. +The client updates at a dynamic time step, with the exception of prediction code (which always runs at the same fixed time step as the server, attempting to maintain a "somewhat deterministic" relationship between the two simulations). +The prediction runs in the [PredictedSimulationSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.PredictedSimulationSystemGroup.html), which applies this unique fixed time step for prediction. + +**The `ClientServerTickRate` configuration is sent (by the server, to the client) during the initial connection handshake. The client prediction loop runs at the exact same `SimulationTickRate` as the server (as mentioned).** ## Standalone builds +When you build a standalone game, Netcode uses the __DOTS Settings__ in the __Project Settings__ window to: +- To decide which type of build to make (only valid for standalone player builds). +- To choose mode-specific baking settings. -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. +### Building standalone servers +In order to build standalone server, you need to switch to a `Dedicated Server` platform. When building a server, the `UNITY_SERVER` define is set automatically (**and also automatically set in the editor**).
+The `DOTS` project setting will reflect this change, by using the setting for the server build type. -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. +### Building standalone client +When using a normal standalone player target (i.e Windows), it is possible to select the type of build to make (in the `DOTS` project setting): +- A `client-only` build. The `UNITY_CLIENT` define will be set in the build (**but not in-editor**). +- A `client/server` build. Neither the `UNITY_CLIENT`, nor the `UNITY_SERVER` are set (i.e. not in built players, nor in-editor). -## World migration +For either build type, specific baking filters can be specified in the `DOTS` project setting. + +## World migration Sometimes you want to be able to destroy the world you are in and spin up another world without loosing your connection state. In order to do this we supply a DriverMigrationSystem, that allows a user to Store and Load Transport related information so a smooth world transition can be made. ``` diff --git a/Documentation~/command-stream.md b/Documentation~/command-stream.md index 875fec4..8a09916 100644 --- a/Documentation~/command-stream.md +++ b/Documentation~/command-stream.md @@ -1,20 +1,85 @@ # 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. +The client continuously sends a command stream to the server when the `NetworkStreamConnection` is tagged to be "in-game". This stream includes all inputs, and acknowledgements of the last received snapshot. +Thus, the connection will be kept alive, even if the client does not have controlled entities, and does not generate any inputs that need to be transmitted to the server. +The command packet is still sent at a regular interval (every full simulated tick), to automatically acknowledge received snapshots, and to report other important information to the server. +## Creating inputs (i.e. commands) To create a new input type, create a struct that implements the `ICommandData` interface. To implement that interface you need to provide a property for accessing the `Tick`. -The serialization and registration code for the `ICommandData` will be generated automatically, but it is also possible to disable that and write the serialization manually. +The serialization and registration code for the `ICommandData` will be generated automatically, but it is also possible to disable that and write the serialization [manually](command-stream.md#manual-serialization). -If you add your `ICommandData` component to a ghost which has `Has Owner` and `Support Auto Command Target` enabled in the autoring component the commands for that ghost will automatically be sent if the ghost is owned by you, is predicted, and [AutoCommandTarget](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.AutoCommandTarget.html).Enabled has not been set to false. +The `ICommandData` buffer can be added to the entity controlled by the player either at baking time (using an authoring component) or at runtime.
In the latter, make sure the dynamic buffer is present on both server and client. -If you are not using `Auto Command Target`, your game code must 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. +### Handling input on the client +The client is only responsible for polling the input source and add `ICommand` to buffer for the entities who it control.
+The queued commands are then automatically sent at regular interval by `CommandSendPacketSystem`. -You can have multiple command systems, and NetCode selects the correct one based on the `ICommandData` type of the entity that points to `CommandTargetComponent`. +The systems responsible for writing to the command buffers must all run inside the [GhostInputSystemGroup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostInputSystemGroup.html). -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. +### Receiving commands on the server +`ICommamdData` are automatically received by the server by the `NetworkStreamReceiveSystem` and added to the `IncomingCommandDataStreamBufferComponent` buffer. The `CommandReceiveSystem` is then responsible +to dispatch the command data to the target entity (which the command belong to). +>![NOTE] The server must only receive commands from the clients. It should never overwrite or change the input received by the client. -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. +## Automatic handling of commands. The AutoCommandTarget component. +If you add your `ICommandData` component to a ghost (for which the following options has been enabled in the `GhostAuthoringComponent): +1. `Has Owner` set +2. `Support Auto Command Target` + +enable-autocommand + +the commands for that ghost will **automatically be sent to the server**. Obviously, the following rules apply: +- the ghost must be owned by your client (requiring the server to set the `GhostOwnerComponent` to your `NetworkIdComponent.Value`), +- the ghost is `Predicted` or `OwnerPredicted` (i.e. you therefore cannot use an `ICommandData` to control interpolated ghosts), +- the [AutoCommandTarget](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.AutoCommandTarget.html).Enabled flag is set to true. + +If you are not using `Auto Command Target`, your game code must 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 for Entities will select the correct one (based on the `ICommandData` type of the entity that points to `CommandTargetComponent`). + +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 ring-buffer for you). + +>~[NOTE] When you update the state of your simulation inside the prediction loop, you must rely only on the commands present in the `ICommandData` buffer (for a given input type). +Polling input directly, by using UnityEngine.Input or other similar method, or relying on input information not present in the struct implementing the `ICommandData` interface may cause client +mis-prediction.
+ +## Checking which ghost entities are owned by the player, on the client. +> [!NOTE] +It is required you use (and implement) the `GhostOwnerComponent` functionality, for commands to work properly. For example: By checking the 'Has Owner' checkbox in the `GhostAuthoringComponent`. + +**On the client, it is very common to want to lookup (i.e. query for) entities that are owned by the local player.** +This is problematic, as multiple ghosts may have the same `CommandBuffer` as your "locally owned" ghost (e.g. when using [Remove Player Prediction](prediction.md#remote-players-prediction), _every other "player" ghost_ will have this buffer), +and your input systems (that populate the input command buffer) may accidentally clobber other players buffers. + +There are two ways to handle this properly: + +### Use the new `GhostOwnerIsLocal` component (PREFERRED) +All ghosts have a special enableable component, `GhostOwnerIsLocal` that you can now use to filter out ghosts not owned by "you". + +For example: +```c# +Entities + .WithAll() + .ForEach((ref MyComponent myComponent)=> + { + // your logic here will be applied only to the entities onwed by "you" (the local player). + }).Run(); +``` +### Use the GhostOwnerComponent +You can filter the entities manually by checking that the `GhostOwnerComponent.NetworkId` of the entity equals the `NetworkId` of the player. + +```c# +var localPlayerId = GetSingleton().Value; +Entities + .WithAll() + .ForEach((ref MyComponent myComponent, in GhostOwnerComponent owner)=> + { + if(owner.NetworkId == localPlayerId) + { + // your logic here will be applied only to the entitis onwed by the local player. + } + }).Run(); +``` ## Automatic command input setup using IInputComponentData @@ -88,13 +153,8 @@ public partial class GatherInputs : SystemBase //... var networkId = GetSingleton().Value; - Entities - .WithName("GatherInput") - .ForEach((ref PlayerInput inputData, ref GhostOwnerComponent owner) => + Entities.WithName("GatherInput").WithAll().ForEach((ref PlayerInput inputData) => { - if (owner.NetworkId != networkId) - return; - inputData = default; if (jump) @@ -148,12 +208,13 @@ public partial struct MyCommandSendCommandSystem : ISystem { CommandSendSystem m_CommandSend; [BurstCompile] - struct SendJob : IJobEntityBatch + struct SendJob : IJobChunk { public CommandSendSystem.SendJobData data; - public void Execute(ArchetypeChunk chunk, int orderIndex) + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, + bool useEnabledMask, in v128 chunkEnabledMask) { - data.Execute(chunk, orderIndex); + data.Execute(chunk, unfilteredChunkIndex); } } [BurstCompile] @@ -179,12 +240,13 @@ public partial struct MyCommandReceiveCommandSystem : ISystem { CommandReceiveSystem m_CommandRecv; [BurstCompile] - struct ReceiveJob : IJobEntityBatch + struct ReceiveJob : IJobChunk { public CommandReceiveSystem.ReceiveJobData data; - public void Execute(ArchetypeChunk chunk, int orderIndex) + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, + bool useEnabledMask, in v128 chunkEnabledMask) { - data.Execute(chunk, orderIndex); + data.Execute(chunk, unfilteredChunkIndex); } } [BurstCompile] diff --git a/Documentation~/debugging.md b/Documentation~/debugging.md new file mode 100644 index 0000000..1d3cd2a --- /dev/null +++ b/Documentation~/debugging.md @@ -0,0 +1,7 @@ +# Debugging + +| **Topic** | **Description** | +|:-------------------------------------| :----------------------- | +| **[PlayModeTool](playmode-tool.md)** | Logging in Netcode for Entities | +| **[Logging](logging.md)** | Logging in Netcode for Entities | +| **[Metrics](metrics.md)** | Metrics in Netcode for Entities | diff --git a/Documentation~/entities-list.md b/Documentation~/entities-list.md index cdd2f87..0664e1f 100644 --- a/Documentation~/entities-list.md +++ b/Documentation~/entities-list.md @@ -6,212 +6,273 @@ This page contains a list of all entities used by the Netcode package. A connection entity is created for each network connection. You can think of these entities as your network socket, but they do contain a bit more data and configuration for other Netcode systems. -| Component | Description | Condition | -| --------- | ----------- | --------- | -|[__NetworkStreamConnection__](xref:Unity.NetCode.NetworkStreamConnection) | The Unity Transport `NetworkConnection` used to send and receive data. -|[__NetworkSnapshotAckComponent__](xref:Unity.NetCode.NetworkSnapshotAckComponent) | Data used to keep track of what data has been received. -|[__CommandTargetComponent__](xref:Unity.NetCode.CommandTargetComponent) | A pointer to the entity where commands should be read from or written too. The target entity must have a `ICommandData` component on it. -|[__IncomingRpcDataStreamBufferComponent__](xref:Unity.NetCode.IncomingRpcDataStreamBufferComponent) | A buffer of received RPC commands which will be processed by the RpcSystem. Intended for internal use only. -|[__IncomingCommandDataStreamBufferComponent__](xref:Unity.NetCode.IncomingCommandDataStreamBufferComponent) | A buffer of received commands which will be processed by a generated CommandReceiveSystem. Intended for internal use only. | Server only -|[__OutgoingCommandDataStreamBufferComponent__](xref:Unity.NetCode.OutgoingCommandDataStreamBufferComponent) | A buffer of commands generated be a CommandSendSystem which will be sent to the server. Intended for internal use only. | Client only -|[__IncomingSnapshotDataStreamBufferComponent__](xref:Unity.NetCode.IncomingSnapshotDataStreamBufferComponent) | A buffer of received snapshots which will be processed by the GhostReceiveSystem. Intended for internal use only. | Client only -|[__OutgoingRpcDataStreamBufferComponent__](xref:Unity.NetCode.OutgoingRpcDataStreamBufferComponent) | A buffer of RPC commands which should be sent by the RpcSystem. Intended for internal use only, use an `RpcQueue` or `IRpcCommand` component to write RPC data. -|[__NetworkIdComponent__](xref:Unity.NetCode.NetworkIdComponent) | The network id is used to uniquely identify a connection. If this component does not exist the connection is not yet complete. | Added automatically when connection is complete -|[__NetworkStreamInGame__](xref:Unity.NetCode.NetworkStreamInGame) | A component used to signal that a connection should send and receive snapshots and commands. Before adding this component the connection only processes RPCs. | Added by game logic to start sending snapshots and commands. -|[__NetworkStreamRequestDisconnect__](xref:Unity.NetCode.NetworkStreamRequestDisconnect) | A component used to signal that the game logic wants to close the connection. | Added by game logic to disconnect. -|[__NetworkStreamDisconnected__](xref:Unity.NetCode.NetworkStreamDisconnected) | A component used to signal that a connection has been disconnected. The entity will exist with this component for one frame, after that it is automatically deleted. | Added automatically when a connection is disconnected. -|[__NetworkStreamSnapshotTargetSize__](xref:Unity.NetCode.NetworkStreamSnapshotTargetSize) | Used to tell the `GhostSendSystem` on the server to use a non-default packet size for snapshots. | Added by game logic to change snapshot packet size. -|[__GhostConnectionPosition__](xref:Unity.NetCode.GhostConnectionPosition) | Used by the distance based importance system to scale importance of ghosts based on distance from the player. | Added by game logic to specify the position of the player for a connection. -|[__PrespawnSectionAck__](xref:Unity.NetCode.PrespawnSectionAck) | Used by the server to track which subscenes the client has loaded. | Server only -|[__EnablePacketLogging__](xref:Unity.NetCode.EnablePacketLogging) | Added by game logic to enable packet dumps for a single connection. | Only when enabling packet dumps +| Component | Description | Condition | +|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| +| [__NetworkStreamConnection__](xref:Unity.NetCode.NetworkStreamConnection) | The Unity Transport `NetworkConnection` used to send and receive data. | +| [__NetworkSnapshotAckComponent__](xref:Unity.NetCode.NetworkSnapshotAckComponent) | Data used to keep track of what data has been received. | +| [__CommandTargetComponent__](xref:Unity.NetCode.CommandTargetComponent) | A pointer to the entity where commands should be read from or written too. The target entity must have a `ICommandData` component on it. | +| [__IncomingRpcDataStreamBufferComponent__](xref:Unity.NetCode.IncomingRpcDataStreamBufferComponent) | A buffer of received RPC commands which will be processed by the `RpcSystem`. Intended for internal use only. | +| [__IncomingCommandDataStreamBufferComponent__](xref:Unity.NetCode.IncomingCommandDataStreamBufferComponent) | A buffer of received commands which will be processed by a generated `CommandReceiveSystem`. Intended for internal use only. | Server only | +| [__OutgoingCommandDataStreamBufferComponent__](xref:Unity.NetCode.OutgoingCommandDataStreamBufferComponent) | A buffer of commands generated be a `CommandSendSystem` which will be sent to the server. Intended for internal use only. | Client only | +| [__IncomingSnapshotDataStreamBufferComponent__](xref:Unity.NetCode.IncomingSnapshotDataStreamBufferComponent) | A buffer of received snapshots which will be processed by the `GhostReceiveSystem`. Intended for internal use only. | Client only | +| [__OutgoingRpcDataStreamBufferComponent__](xref:Unity.NetCode.OutgoingRpcDataStreamBufferComponent) | A buffer of RPC commands which should be sent by the `RpcSystem`. Intended for internal use only, use an `RpcQueue` or `IRpcCommand` component to write RPC data. | +| [__NetworkIdComponent__](xref:Unity.NetCode.NetworkIdComponent) | The network id is used to uniquely identify a connection. If this component does not exist, the connection process has not yet completed. | Added automatically when connection is complete | +| [__NetworkStreamInGame__](xref:Unity.NetCode.NetworkStreamInGame) | A component used to signal that a connection should send and receive snapshots and commands. Before adding this component, the connection only processes RPCs. | Added by game logic to start sending snapshots and commands. | +| [__NetworkStreamRequestDisconnect__](xref:Unity.NetCode.NetworkStreamRequestDisconnect) | A component used to signal that the game logic wants to close the connection. | Added by game logic to disconnect. | +| [__NetworkStreamSnapshotTargetSize__](xref:Unity.NetCode.NetworkStreamSnapshotTargetSize) | Used to tell the `GhostSendSystem` on the server to use a non-default packet size for snapshots. | Added by game logic to change snapshot packet size. | +| [__GhostConnectionPosition__](xref:Unity.NetCode.GhostConnectionPosition) | Used by the distance based importance system to scale importance of ghosts based on distance from the player. | Added by game logic to specify the position of the player for a connection. | +| [__PrespawnSectionAck__](xref:Unity.NetCode.PrespawnSectionAck) | Used by the server to track which subscenes the client has loaded. | Server only | +| [__EnablePacketLogging__](xref:Unity.NetCode.EnablePacketLogging) | Added by game logic to enable packet dumps for a single connection. | Only when enabling packet dumps | ## Ghost A ghost is an entity on the server which is ghosted (replicated) to the clients. It is always instantiated from a ghost prefab and has user defined data in addition to the components listed here which control its behavior. -| Component | Description | Condition | -|---------------------------------------------------------------------------------------------------| ----------- | --------- | -| [__GhostComponent__](xref:Unity.NetCode.GhostComponent) | Identifying an entity as a ghost. -| [__GhostTypeComponent__](xref:Unity.NetCode.GhostTypeComponent) | The type this ghost belongs to. -| __GhostCleanupComponent__ | This component exists for only for internal use in the NetCode package. Used to track despawn of ghosts on the server. | Server only -| [__SharedGhostTypeComponent__](xref:Unity.NetCode.SharedGhostTypeComponent) | A shared component version of the `GhostTypeComponent`, used on the server only to make sure different ghost types never share the same chunk. | Server only -| [__SnapshotData__](xref:Unity.NetCode.SnapshotData) | A buffer with meta data about the snapshots received from the server. | Client only -| [__SnapshotDataBuffer__](xref:Unity.NetCode.SnapshotDataBuffer) | A buffer with the raw snapshot data received from the server. | Client only -| [__SnapshotDynamicDataBuffer__](xref:Unity.NetCode.SnapshotDynamicDataBuffer) | A buffer with the raw snapshot data for buffers received from the server. | Client only, ghosts with buffers only -| [__PredictedGhostComponent__](xref:Unity.NetCode.PredictedGhostComponent) | Identify predicted ghosts. On the server all ghosts are considered predicted and have this component. | Predicted only -| [__GhostDistancePartition__](xref:Unity.NetCode.GhostDistancePartition) | Added to all ghosts with a `Translation` when distance based importance is used. | Only for distance based importance -| [__GhostDistancePartitionShared__](xref:Unity.NetCode.GhostDistancePartitionShared) | Added to all ghosts with a `Translation` when distance based importance is used. | Only for distance based importance -| [__GhostPrefabMetaDataComponent__](xref:Unity.NetCode.GhostPrefabMetaDataComponent) | The meta data for a ghost, adding durin conversion and used to setup serialiation. This is not required on ghost instances, only on prefabs, but it is only removed from pre-spawned right now. | Not in pre-spawned -| [__GhostChildEntityComponent__](xref:Unity.NetCode.GhostChildEntityComponent) | Disable the serialization of this entity because it is part of a ghost group and will be serialized as part of that. | Only children in ghost groups -| [__GhostGroup__](xref:Unity.NetCode.GhostGroup) | Added to all ghosts which can be the owner of a ghost group. Must be added to the prefab at conversion time. | Only ghost group root -| [__PredictedGhostSpawnRequestComponent__](xref:Unity.NetCode.PredictedGhostSpawnRequestComponent) | This instance is not a ghost received from the server, but a request to predictively spawn a ghost which the client expects the server to spawn soon. Prefab entity references on clients will have this component added automatically so anything they spawn themselves will be by default predict spawned. -| [__GhostOwnerComponent__](xref:Unity.NetCode.GhostOwnerComponent) | Identiy the owner of a ghost, specified as a network id. | Optional -| [__AutoCommandTarget__](xref:Unity.NetCode.AutoCommandTarget) | Automatically send all `ICommandData` if the ghost is owned by the current connection, `AutoCommandTarget.Enabled` is true and the ghost is predicted. | Optional -| [__SubSceneGhostComponentHash__](xref:Unity.NetCode.SubSceneGhostComponentHash) | The hash of all pre-spawned ghosts in a subscene, used for sorting and grouping. This is a shared component. | Only pre-spawned -| [__PreSpawnedGhostIndex__](xref:Unity.NetCode.PreSpawnedGhostIndex) | Unique index of a pre-spawned ghost within a subscene. | Only pre-spawned -| [__PrespawnGhostBaseline__](xref:Unity.NetCode.PrespawnGhostBaseline) | The snapshot data a pre-spawned ghost had in the scene data. Used as a fallback baseline. | Only pre-spawned -| [__GhostPrefabRuntimeStrip__](xref:Unity.NetCode.GhostPrefabRuntimeStrip) | Added to prefabs and pre-spawned during conversion to client and server data to trigger runtime stripping of component. | Only on prefabs in client and server scenes before they are initialized -| [__LiveLinkPrespawnSectionReference__](xref:Unity.NetCode.LiveLinkPrespawnSectionReference) | Component present in editor on the scene section entity when the sub-scene is open for edit. | Only in Editor - -|[__PreSerializedGhost__](xref:Unity.NetCode.PreSerializedGhost) | Enable pre-serialization for a ghost, added at conversion time based on ghost settings. | Only ghost using pre-serialization -|[__SwitchPredictionSmoothing__](xref:Unity.NetCode.SwitchPredictionSmoothing) | Added temporarily when switching a ghost between predicted / interpolated with a transition time to handle transform smoothing. | Only ghost in the process of switching prediction mode -|[__PrefabDebugName__](xref:Unity.NetCode.PrefabDebugName) | Name of the prefab used for debugging. | Only on prefabs when NETCODE_DEBUG is enabled -|[__GhostPhysicsProxyReference__](xref:Unity.NetCode.GhostPhysicsProxyReference) | A reference to the client-only physics entity proxy. Add at runtime when Predicted Physics is enabled to all ghosts with a GenerateGhostPhysicsProxy when the proxy is spawned. | Client only. -|[__GenerateGhostPhysicsProxy__](xref:Unity.NetCode.GenerateGhostPhysicsProxy) | Configure the physics proxy entity to spawn for a ghost when predicted physics is enabled. | Client only. - +| Component | Description | Condition | +|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| +| [__GhostComponent__](xref:Unity.NetCode.GhostComponent) | Identifying an entity as a ghost. | | +| [__GhostTypeComponent__](xref:Unity.NetCode.GhostTypeComponent) | The type this ghost belongs to. | | +| __GhostCleanupComponent__ | This component exists for only for internal use in the Netcode for Entities package. Used to track despawn of ghosts on the server. | Server only | +| [__SharedGhostTypeComponent__](xref:Unity.NetCode.SharedGhostTypeComponent) | A shared component version of the `GhostTypeComponent` to make sure different ghost types never share the same chunk. | +| [__SnapshotData__](xref:Unity.NetCode.SnapshotData) | A buffer with meta data about the snapshots received from the server. | Client only | +| [__SnapshotDataBuffer__](xref:Unity.NetCode.SnapshotDataBuffer) | A buffer with the raw snapshot data received from the server. | Client only | +| [__SnapshotDynamicDataBuffer__](xref:Unity.NetCode.SnapshotDynamicDataBuffer) | A buffer with the raw snapshot data for buffers received from the server. | Client only, ghosts with buffers only | +| [__PredictedGhostComponent__](xref:Unity.NetCode.PredictedGhostComponent) | Identify predicted ghosts. On the server all ghosts are considered predicted and have this component. | Predicted only | +| [__GhostDistancePartition__](xref:Unity.NetCode.GhostDistancePartition) | Added to all ghosts with a `LocalTransform`, when distance based importance is used. | Only for distance based importance | +| [__GhostDistancePartitionShared__](xref:Unity.NetCode.GhostDistancePartitionShared) | Added to all ghosts with a `LocalTransform`, when distance based importance is used. | Only for distance based importance | +| [__GhostPrefabMetaDataComponent__](xref:Unity.NetCode.GhostPrefabMetaDataComponent) | The meta data for a ghost, adding during conversion, and used to setup serialization. This is not required on ghost instances, only on prefabs, but it is only removed from pre-spawned right now. | Not in pre-spawned | +| [__GhostChildEntityComponent__](xref:Unity.NetCode.GhostChildEntityComponent) | Disable the serialization of this entity because it is part of a ghost group (and therefore will be serialized as part of that group). | Only children in ghost groups | +| [__GhostGroup__](xref:Unity.NetCode.GhostGroup) | Added to all ghosts which can be the owner of a ghost group. Must be added to the prefab at conversion time. | Only ghost group root | +| [__PredictedGhostSpawnRequestComponent__](xref:Unity.NetCode.PredictedGhostSpawnRequestComponent) | This instance is not a ghost received from the server, but a request to predictively spawn a ghost (which the client expects the server to spawn authoritatively, soon). Prefab entity references on clients will have this component added automatically, so anything they spawn themselves will be by default predict spawned. | +| [__GhostOwnerComponent__](xref:Unity.NetCode.GhostOwnerComponent) | Identifies the owner of a ghost, specified as a "Network Id". | Optional | +| [__GhostOwnerIsLocal__](xref:Unity.NetCode.GhostOwnerIsLocal) | An enableable tag component used to track if a ghost (with an owner) is owned by the local host or not. | Optional | +| [__AutoCommandTarget__](xref:Unity.NetCode.AutoCommandTarget) | Automatically send all `ICommandData` if the ghost is owned by the current connection, `AutoCommandTarget.Enabled` is true, and the ghost is predicted. | Optional | +| [__SubSceneGhostComponentHash__](xref:Unity.NetCode.SubSceneGhostComponentHash) | The hash of all pre-spawned ghosts in a subscene, used for sorting and grouping. This is a shared component. | Only pre-spawned | +| [__PreSpawnedGhostIndex__](xref:Unity.NetCode.PreSpawnedGhostIndex) | Unique index of a pre-spawned ghost within a subscene. | Only pre-spawned | +| [__PrespawnGhostBaseline__](xref:Unity.NetCode.PrespawnGhostBaseline) | The snapshot data a pre-spawned ghost had in the scene data. Used as a fallback baseline. | Only pre-spawned | +| [__GhostPrefabRuntimeStrip__](xref:Unity.NetCode.GhostPrefabRuntimeStrip) | Added to prefabs and pre-spawned during conversion to client and server data to trigger runtime stripping of component. | Only on prefabs in client and server scenes before they are initialized | +| __PrespawnSceneExtracted__ | Component present in editor on the scene section entity, when the sub-scene is open for edit. Intended for internal use only. | Only in Editor | +| [__PreSerializedGhost__](xref:Unity.NetCode.PreSerializedGhost) | Enable pre-serialization for a ghost. Added at conversion time based on ghost settings. | Only ghost using pre-serialization | +| [__SwitchPredictionSmoothing__](xref:Unity.NetCode.SwitchPredictionSmoothing) | Added temporarily when using "Prediction Switching" (i.e. when switching a ghost from predicted to interpolated (or vice-versa), with a transition time to handle transform smoothing. | Only ghost in the process of switching prediction mode | +| [__PrefabDebugName__](xref:Unity.NetCode.PrefabDebugName) | Name of the prefab, used for debugging. | Only on prefabs when `NETCODE_DEBUG` is enabled | ### Placeholder ghost - When a ghost is received but is not yet supposed to be spawned the client will create a placeholder to store the data until it is time to spawn it. The placeholder ghosts only exist on clients and have these components -| Component | Description | Condition | -| --------- | ----------- | --------- | -|[__GhostComponent__](xref:Unity.NetCode.GhostComponent) | Identifying an entity as a ghost. -|[__PendingSpawnPlaceholderComponent__](xref:Unity.NetCode.PendingSpawnPlaceholderComponent) | Identify the ghost as a placeholder and not a proper ghost. -|[__SnapshotData__](xref:Unity.NetCode.SnapshotData) | A buffer with meta data about the snapshots received from the server. | Client only -|[__SnapshotDataBuffer__](xref:Unity.NetCode.SnapshotDataBuffer) | A buffer with the raw snapshot data received from the server. -|[__SnapshotDynamicDataBuffer__](xref:Unity.NetCode.SnapshotDynamicDataBuffer) | A buffer with the raw snapshot data for buffers received from the server. | Ghosts with buffers only + +| Component | Description | Condition | +|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|--------------------------| +| [__GhostComponent__](xref:Unity.NetCode.GhostComponent) | Identifying an entity as a ghost. | +| [__PendingSpawnPlaceholderComponent__](xref:Unity.NetCode.PendingSpawnPlaceholderComponent) | Identify the ghost as a placeholder and not a proper ghost. | +| [__SnapshotData__](xref:Unity.NetCode.SnapshotData) | A buffer with meta data about the snapshots received from the server. | Client only | +| [__SnapshotDataBuffer__](xref:Unity.NetCode.SnapshotDataBuffer) | A buffer with the raw snapshot data received from the server. | +| [__SnapshotDynamicDataBuffer__](xref:Unity.NetCode.SnapshotDynamicDataBuffer) | A buffer with the raw snapshot data for buffers received from the server. | Ghosts with buffers only | ### Client-Only physics proxy -When predicted physics is enabled, it is possible to make physicallty simulated ghosts to interact with physics object present in only on the client (client-only physics world) by -spawning kinematic enties the client-only physics world, driven by the simulated ghost. -| Component | Description | Condition | -| --------- | ----------- | --------- | -[__PhysicsProxyGhostDriver__](xref:Unity.NetCode.PhysicsProxyGhostDriver) | A component that referene the ghost which drive the proxy and let configure how the ghost and the proxy are synched. +it is possible to make "physically simulated" ghosts interact with physics objects present only on the client-only physics world (e.g. particles, debris, cosmetic environmental destruction), by spawning kinematic copies of the colliders present on the predicted, simulated ghosts, synced to them. +Note, however, that this synchronisation can only go one way. I.e. Client-only physics worlds cannot influence the server authoritative ghost (by definition). + +| Component | Description | +|-----------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| +| [__CustomPhysicsProxyDriver__](xref:Unity.NetCode.CustomPhysicsProxyDriver) | A component that reference the ghost which drive the proxy and let configure how the ghost and the proxy are synced. | ## RPC -RPC entities are created with a send request in order to send RPCs. When they are received the system will create entities with the RPC component and a receive request. +RPC entities are created with a send request in order to send RPCs. When they are received, the system will create entities with the RPC component, and a "receive request" (i.e. an `ReceiveRpcCommandRequestComponent` component). -| Component | Description | Condition | -| --------- | ----------- | --------- | -|[__IRpcCommand__](xref:Unity.NetCode.IRpcCommand) | A specific implementation of the IRpcCommand interface. -|[__SendRpcCommandRequestComponent__](xref:Unity.NetCode.SendRpcCommandRequestComponent) | Specify that this RPC is to be sent. | Added by game logic, only for sending. -|[__ReceiveRpcCommandRequestComponent__](xref:Unity.NetCode.ReceiveRpcCommandRequestComponent) | Specify that this RPC is received. | Added automatically, only for receiving. +| Component | Description | Condition | +|-----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__IRpcCommand__](xref:Unity.NetCode.IRpcCommand) | A specific implementation of the IRpcCommand interface. | | +| [__SendRpcCommandRequestComponent__](xref:Unity.NetCode.SendRpcCommandRequestComponent) | Specify that this RPC is to be sent (and thus the RPC entity destroyed). | Added by game logic, only for sending. Deleted automatically. | +| [__ReceiveRpcCommandRequestComponent__](xref:Unity.NetCode.ReceiveRpcCommandRequestComponent) | Specify that this RPC entity has been received (and thus this RPC entity was recently created). | Added automatically, only for receiving. Must be processed and then deleted by game-code, or you'll leak entities into the world. See `WarnAboutStaleRpcSystem`. | ### Netcode RPCs -| Component | Description | -| --------- | ----------- | -| __RpcSetNetworkId__ | Special RPC only sent on connect. -| __ClientServerTickRateRefreshRequest__ | Special RPC only sent on connect. -| __HeartbeatComponent__ | Send at regular intervals when not in game to make sure the connection does not time out. -| __StartStreamingSceneGhosts__ | Sent from client to server when a subscene has been loaded. -| __StopStreamingSceneGhosts__ | Sent from client to server when a subscene will be unloaded. +| Component | Description | +|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| __RpcSetNetworkId__ | Special RPC only sent on connect. | +| __ClientServerTickRateRefreshRequest__ | Special RPC only sent on connect. | +| __HeartbeatComponent__ | Send at regular intervals when not in game to make sure the connection does not time out. | +| __StartStreamingSceneGhosts__ | Sent from client to server when a subscene has been loaded. Used to instruct the server to start sending prespawned ghosts for that scene. | +| __StopStreamingSceneGhosts__ | Sent from client to server when a subscene will be unloaded. Used to instruct the server to stop sending prespawned ghosts that live in that scene. | ### CommandData Every connection which is receiving commands from a client needs to have an entity to hold the command data. This can be a ghost, the connection entity itself or some other entity. -| Component | Description | Condition | -| --------- | ----------- | --------- | -|[__ICommandData__](xref:Unity.NetCode.ICommandData)| A specific implemenation of the ICommandData interface. This can be added to any entity, the connections `CommandTargetComponent` must point to an entity containing this. -|[__CommandDataInterpolationDelay__](xref:Unity.NetCode.CommandDataInterpolationDelay)| Optional component used to access the interpolation delay in order to implement lag compensation on hte server. Also exists on predicted clients but always has an interpolation delay of 0 there. | Added by game logic, predicted only - -### Netcode CommandData -| Component | Description | -| --------- | ----------- | -| __NullCommandData__ | Special CommandData sent when command target is null to make sure ping and ack messages still work. +| Component | Description | Condition | +|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| [__ICommandData__](xref:Unity.NetCode.ICommandData) | A specific implementation of the ICommandData interface. This can be added to any entity, the connections `CommandTargetComponent` must point to an entity containing this. | | +| [__CommandDataInterpolationDelay__](xref:Unity.NetCode.CommandDataInterpolationDelay) | Optional component used to access the interpolation delay, in order to implement lag compensation on the server. Also exists on predicted clients, but always has an interpolation delay of 0 there. | Added by game logic, predicted only | ## SceneSection When using pre-spawned ghosts Netcode will add some components to the SceneSection entity containing the ghosts. -| Component | Description | Condition | -| --------- | ----------- | --------- | -|[__SubSceneWithPrespawnGhosts__](xref:Unity.NetCode.SubSceneWithPrespawnGhosts)| Added during convertion to track which section contains pre-spawned ghosts. -|[__SubSceneWithGhostStateComponent__](xref:Unity.NetCode.SubSceneWithGhostStateComponent)| Used to track unloading of scenes. | Processed sections. -|[__PrespawnsSceneInitialized__](xref:Unity.NetCode.PrespawnsSceneInitialized)| Tag to specify that a section has been processed. | Processed sections. -|[__SubScenePrespawnBaselineResolved__](xref:Unity.NetCode.SubScenePrespawnBaselineResolved)| Tag to specify that a section has resolved baselines. This is a partially initialized state. | Partially processed sections. + +| Component | Description | Condition | +|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|-------------------------------| +| [__SubSceneWithPrespawnGhosts__](xref:Unity.NetCode.SubSceneWithPrespawnGhosts) | Added during conversion to track which section contains pre-spawned ghosts. | | +| [__SubSceneWithGhostStateComponent__](xref:Unity.NetCode.SubSceneWithGhostStateComponent) | Used to track unloading of scenes. | Processed sections. | +| [__PrespawnsSceneInitialized__](xref:Unity.NetCode.PrespawnsSceneInitialized) | Tag to specify that a section has been processed. | Processed sections. | +| [__SubScenePrespawnBaselineResolved__](xref:Unity.NetCode.SubScenePrespawnBaselineResolved) | Tag to specify that a section has resolved baselines. This is a partially initialized state. | Partially processed sections. | ## Netcode created singletons ### PredictedGhostSpawnList A singleton with a list of all predicted spawned ghosts which are waiting for a ghost from the server. This is needed when writing logic matching an incoming ghost with a pre-spawned one. -| Component | Description | -| --------- | ----------- | -|[__PredictedGhostSpawnList__](xref:Unity.NetCode.PredictedGhostSpawnList)| A tag for finding the predicted spawn list. -|[__PredictedGhostSpawn__](xref:Unity.NetCode.PredictedGhostSpawn)| A lis of all predictively spawned ghosts. + +| Component | Description | +|---------------------------------------------------------------------------|---------------------------------------------| +| [__PredictedGhostSpawnList__](xref:Unity.NetCode.PredictedGhostSpawnList) | A tag for finding the predicted spawn list. | +| [__PredictedGhostSpawn__](xref:Unity.NetCode.PredictedGhostSpawn) | A lis of all predictively spawned ghosts. | ### Ghost Collection -| Component | Description | -| --------- | ----------- | -|[__GhostCollection__](xref:Unity.NetCode.GhostCollection) | Identify the singleton containing ghost prefabs. -|[__GhostCollectionPrefab__](xref:Unity.NetCode.GhostCollectionPrefab) | A list of all ghost prefabs which can be instantiated. -|[__GhostCollectionPrefabSerializer__](xref:Unity.NetCode.GhostCollectionPrefabSerializer) | A list of serializers for all ghost prefabs. The index in this list is identical to `GhostCollectionPrefab`, but it can temporarily have fewer entries when a prefab is loading. This references a range in the `GhostCollectionComponentIndex` list. -|[__GhostCollectionComponentType__](xref:Unity.NetCode.GhostCollectionComponentType) | The set of serializers in the `GhostComponentSerializer.State` which can be used for a given type. This is used internally to setup the `GhostCollectionPrefabSerializer`. -|[__GhostCollectionComponentIndex__](xref:Unity.NetCode.GhostCollectionComponentIndex) | A list of mappings from prefab serializer index to a child entity index and a `GhostComponentSerializer.State` index. This mapping is there to avoid having to duplicate the full serialization state for each prefab using the same component. -|[__GhostComponentSerializer.State__](xref:Unity.NetCode.GhostComponentSerializer.State) | Serialization state - including function pointers for serialization - for a component type and variant. There can be more than one entry for a given component type if there are serialization variants. +| Component | Description | +|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__GhostCollection__](xref:Unity.NetCode.GhostCollection) | Identify the singleton containing ghost prefabs. | +| [__GhostCollectionPrefab__](xref:Unity.NetCode.GhostCollectionPrefab) | A list of all ghost prefabs which can be instantiated. | +| [__GhostCollectionPrefabSerializer__](xref:Unity.NetCode.GhostCollectionPrefabSerializer) | A list of serializers for all ghost prefabs. The index in this list is identical to `GhostCollectionPrefab`, but it can temporarily have fewer entries when a prefab is loading. This references a range in the `GhostCollectionComponentIndex` list. | +| [__GhostCollectionComponentType__](xref:Unity.NetCode.GhostCollectionComponentType) | The set of serializers in the `GhostComponentSerializer.State` which can be used for a given type. This is used internally to setup the `GhostCollectionPrefabSerializer`. | +| [__GhostCollectionComponentIndex__](xref:Unity.NetCode.GhostCollectionComponentIndex) | A list of mappings from prefab serializer index to a child entity index and a `GhostComponentSerializer.State` index. This mapping is there to avoid having to duplicate the full serialization state for each prefab using the same component. | +| [__GhostComponentSerializer.State__](xref:Unity.NetCode.GhostComponentSerializer.State) | Serialization state - including function pointers for serialization - for a component type and variant. There can be more than one entry for a given component type if there are serialization variants. | ### Spawn queue -| Component | Description | -| --------- | ----------- | -|[__GhostSpawnQueueComponent__](xref:Unity.NetCode.GhostSpawnQueueComponent)| Identifier for the ghost spawn queue. -|[__GhostSpawnBuffer__](xref:Unity.NetCode.GhostSpawnBuffer)| A list of ghosts in the spawn queue. This queue is written by the `GhostReceiveSystem` and read by the `GhostSpawnSystem`. A classification system running between those two can change the type of ghost to sapwn and match incomming ghosts with pre-spawned ghosts. -|[__SnapshotDataBuffer__](xref:Unity.NetCode.SnapshotDataBuffer)| Raw snapshot data for the new ghosts in the `GhostSpawnBuffer`. +| Component | Description | +|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__GhostSpawnQueueComponent__](xref:Unity.NetCode.GhostSpawnQueueComponent) | Identifier for the ghost spawn queue. | +| [__GhostSpawnBuffer__](xref:Unity.NetCode.GhostSpawnBuffer) | A list of ghosts in the spawn queue. This queue is written by the `GhostReceiveSystem` and read by the `GhostSpawnSystem`. A classification system running between those two can change the type of ghost to spawn and match incoming ghosts with pre-spawned ghosts. | +| [__SnapshotDataBuffer__](xref:Unity.NetCode.SnapshotDataBuffer) | Raw snapshot data for the new ghosts in the `GhostSpawnBuffer`. | ### NetworkProtocolVersion -| Component | Description | -| --------- | ----------- | -|[__NetworkProtocolVersion__](xref:Unity.NetCode.NetworkProtocolVersion)| The network protocol version for RPCs, ghost component serializers, netcode version and game version. At connection time netcode will validate that the client and server has the same version. +| Component | Description | +|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__NetworkProtocolVersion__](xref:Unity.NetCode.NetworkProtocolVersion) | The network protocol version for RPCs, ghost component serializers, netcode version and game version. At connection time netcode will validate that the client and server has the same version. | ### PrespawnGhostIdAllocator -| Component | Description | -| --------- | ----------- | -|[__PrespawnGhostIdRange__](xref:Unity.NetCode.PrespawnGhostIdRange) | The set of ghost ids assosiated with a subscene. Used by the server to map prspawned ghosts for a subscene to proper ghost ids. +| Component | Description | +|---------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| [__PrespawnGhostIdRange__](xref:Unity.NetCode.PrespawnGhostIdRange) | The set of ghost ids associated with a subscene. Used by the server to map prespawned ghosts for a subscene to proper ghost ids. | ### PrespawnSceneLoaded This singleton is a special kind of ghost without a prefab asset. -| Component | Description | -| --------- | ----------- | -|[__PrespawnSceneLoaded__](xref:Unity.NetCode.PrespawnSceneLoaded) | The set of scenes with pre-spawned ghosts loaded by the server. This is ghosted to clients. + +| Component | Description | +|-------------------------------------------------------------------|---------------------------------------------------------------------------------------------| +| [__PrespawnSceneLoaded__](xref:Unity.NetCode.PrespawnSceneLoaded) | The set of scenes with pre-spawned ghosts loaded by the server. This is ghosted to clients. | ### MigrationTicket -| Component | Description | -| --------- | ----------- | -|[__MigrationTicket__](xref:Unity.NetCode.MigrationTicket) | Created in the new world when using world migration, triggers the restore part of migration. +| Component | Description | +|-----------------------------------------------------------|----------------------------------------------------------------------------------------------| +| [__MigrationTicket__](xref:Unity.NetCode.MigrationTicket) | Created in the new world when using world migration, triggers the restore part of migration. | ### SmoothingAction -| Component | Description | -| --------- | ----------- | -|__SmoothingAction__ | Singleton created when a smothing action is registered in order to enable the smoothing system. +| Component | Description | +|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [__SmoothingAction__](xref:Unity.NetCode.SmoothingAction) | Singleton created when a smothing action is registered in order to enable the smoothing system. | + +### NetworkTimeSystemData +| Component | Description | +|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| __NetworkTimeSystemData__ | Internal singleton, used to store the state of the network time system. | +| __NetworkTimeSystemStats__ | Internal singleton, that track the time scaling applied to the predicted and interpolated tick.
Used to report stats to net debugger. | + +### NetworkTime +| Component | Description | +|---------------------------------------------------|-----------------------------------------------------------------------------------------------------| + | [__NetworkTime__](xref:Unity.NetCode.NetworkTime) | Singleton component that contains all the timing characterist of the client/server simulation loop. | + +### NetDebug +| Component | Description | +|---------------------------------------------|--------------------------------------------------------------------------| +| [__NetDebug__](xref:Unity.NetCode.NetDebug) | Singleton that can be used for debug log and managing the logging level. | + +### NetworkStreamDriver +| Component | Description | +|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__NetworkStreamDriver__](xref:Unity.NetCode.NetworkStreamDriver) | Singleton that can hold a reference to the NetworkDriverStore and that should be used to easily listening for new connection or connecting to server. | + +### RpcCollection + +### GhostPredictionSmoothing +| Component | Description | +|------------------------------|----------------------------------------------------------------------------------------| +| __GhostPredictionSmoothing__ | Singleton used to register the smoothing action used to correct the prediction errors. | + +### GhostPredictionHistoryState +| Component | Description | +|---------------------------------|--------------------------------------------------------------------------------------------| +| __GhostPredictionHistoryState__ | Internal singleton that contains the last predicted full tick state of all predicted ghost | + +### GhostSnapshotLastBackupTick +| Component | Description | +|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| __GhostSnapshotLastBackupTick__ | Internal singleton that contains the last full tick for which a snapshot backup is avaiable. Only present on the client world. | + +### GhostStats +| Component | Description | +|-----------------------------------------|---------------------------------------------------------------------| +| __GhostStats__ | State if the NetDbg tools is connected or not. | +| __GhostStatsCollectionCommand__ | Internal stats data for commands. | +| __GhostStatsCollectionSnapshot__ | Internal stats data used to track sent/received snapshot data. | +| __GhostStatsCollectionPredictionError__ | Record the prediction stats for various ghost/component types pair. | +| __GhostStatsCollectionMinMaxTick__ | | +| __GhostStatsCollectionData__> | Contains internal data pools and other stats system related states. | + +### GhostSendSystemData +| Component | Description | +|-------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| [__GhostSendSystemData__](xref:Unity.NetCode.GhostSendSystemData) | Singleton entity that contains all the tweakable settings for the GhostSendSystem | + +### SpawnedGhostEntityMap +| Component | Description | +|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| [__SpawnedGhostEntityMap__](xref:Unity.NetCode.SpawnedGhostEntityMap) | Singleton that contains the last predicted full tick state of all predicted ghost | + ## User create singletons (settings) ### ClientServerTickRate -| Component | Description | -| --------- | ----------- | -|[__ClientServerTickRate__](xref:Unity.NetCode.ClientServerTickRate) | The tick rate settings for the server. Automatically sent and set on the client based on the values specified on the server. +| Component | Description | +|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| [__ClientServerTickRate__](xref:Unity.NetCode.ClientServerTickRate) | The tick rate settings for the server. Automatically sent and set on the client based on the values specified on the server. | ### ClientTickRate -| Component | Description | -| --------- | ----------- | -|[__ClientTickRate__](xref:Unity.NetCode.ClientTickRate) | The tick rate settings for the client which are not controlled by the server (interpolation time etc.). Use the defaults from `NetworkTimeSystem.DefaultClientTickRate` instead of default values. +| Component | Description | +|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__ClientTickRate__](xref:Unity.NetCode.ClientTickRate) | The tick rate settings for the client which are not controlled by the server (interpolation time etc.). Use the defaults from `NetworkTimeSystem.DefaultClientTickRate` instead of default values. | ### LagCompensationConfig -| Component | Description | -| --------- | ----------- | -|[__LagCompensationConfig__](xref:Unity.NetCode.LagCompensationConfig) | Configuration for the `PhysicsWorldHistory` system which is used to implement lag compensation on the server. If the singleton does not exist `PhysicsWorldHistory` will no be run. +| Component | Description | +|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__LagCompensationConfig__](xref:Unity.NetCode.LagCompensationConfig) | Configuration for the `PhysicsWorldHistory` system which is used to implement lag compensation on the server. If the singleton does not exist `PhysicsWorldHistory` will no be run. | ### GameProtocolVersion -| Component | Description | -| --------- | ----------- | -|[__GameProtocolVersion__](xref:Unity.NetCode.GameProtocolVersion) | The game specific version to use for protcol validation on connection. If this does not exist 0 will be used, but the protocol will still validate netcode version, ghost components and rpcs +| Component | Description | +|-------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__GameProtocolVersion__](xref:Unity.NetCode.GameProtocolVersion) | The game specific version to use for protcol validation on connection. If this does not exist 0 will be used, but the protocol will still validate netcode version, ghost components and rpcs | -### Ghost distance importance -| Component | Description | -| --------- | ----------- | -|[__GhostDistanceImportance__](xref:Unity.NetCode.GhostDistanceImportance)| Settings for distance based importance. If the singleton does not exist distance based importance is not used. +### GhostImportance +| Component | Description | +|-------------------------------------------------------------------|----------------------------------------------------------| +| [__GhostImportance__](xref:Unity.NetCode.GhostDistanceImportance) | Singleton component used to control importance settings. | -### PredictedPhysicsConfig -| Component | Description | -| --------- | ----------- | -|[__PredictedPhysicsConfig__](xref:Unity.NetCode.PredictedPhysicsConfig)| Create a singleton with this to enable and configure predicted physics. +### GhostDistanceData +| Component | Description | +|---------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| +| [__GhostDistanceData__](xref:Unity.NetCode.GhostDistanceImportance) | Settings for distance based importance. If the singleton does not exist distance based importance is not used. | + +### Predicted Physics +| Component | Description | +|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| +| [__PredictedPhysicsNonGhostWorld__](xref:Unity.NetCode.PredictedPhysicsNonGhostWorld) | Singleton component that declare which physics world to use for simulating the client-only physics entities. | ### NetCodeDebugConfig -| Component | Description | -| --------- | ----------- | -|[__NetCodeDebugConfig__](xref:Unity.NetCode.NetCodeDebugConfig)| Create a singleton with this to configure log level and packet dump for all connections. See `EnabledPacketLogging` on the connection for enabling packet dumps for a subset of the connections. + +| Component | Description | +|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__NetCodeDebugConfig__](xref:Unity.NetCode.NetCodeDebugConfig) | Create a singleton with this to configure log level and packet dump for all connections. See `EnabledPacketLogging` on the connection for enabling packet dumps for a subset of the connections. | ### DisableAutomaticPrespawnSectionReporting -| Component | Description | -| --------- | ----------- | -|[__DisableAutomaticPrespawnSectionReporting__](xref:Unity.NetCode.DisableAutomaticPrespawnSectionReporting)| Disable the automatic tracking of which sub-scenes the client has loaded. When creating this singleton you must implement custom logic to make sure the server does not send pre-spawned ghosts which the client has not loaded. + +| Component | Description | +|-------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [__DisableAutomaticPrespawnSectionReporting__](xref:Unity.NetCode.DisableAutomaticPrespawnSectionReporting) | Disable the automatic tracking of which sub-scenes the client has loaded. When creating this singleton you must implement custom logic to make sure the server does not send pre-spawned ghosts which the client has not loaded. | diff --git a/Documentation~/filter.yml b/Documentation~/filter.yml index ffe3c74..8426064 100644 --- a/Documentation~/filter.yml +++ b/Documentation~/filter.yml @@ -24,7 +24,7 @@ apiRules: uidRegex: OnInspectorGUI type: Method - exclude: - uidRegex: Unity\.NetCode\.Tests.* + uidRegex: .*Tests.* type: Namespace - exclude: uidRegex: __COMMAND_NAMESPACE__ diff --git a/Documentation~/getting-started.md b/Documentation~/getting-started.md index ad86cc0..d5fa2d6 100644 --- a/Documentation~/getting-started.md +++ b/Documentation~/getting-started.md @@ -1,287 +1,6 @@ -# 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. +# Getting Started -## Set up the Project -Open the __Unity Hub__ and create a new Project. - ->[!NOTE] -> To use Unity NetCode you must have at least Unity 2020.1.2 installed. - -Open the Package Manager (menu: __Window > Package Manager__). At the top of the window, under __Advanced__, select __Show preview packages__. Add the Entities, Entities Graphics, NetCode, and Transport packages. - ->[!WARNING] -> As of Unity version 2020.1, in-preview packages no longer appear in the Package Manager. To use preview packages, either manually edit your [project manifest](https://docs.unity3d.com/2020.1/Documentation/Manual/upm-concepts.html?_ga=2.181752096.669754589.1597830146-1414726221.1582037216#Manifests) or search for the package in the **Add package from Git URL** field in the Package Manager. For more information, see the [announcement blog for these changes to the Package Manager.](https://blogs.unity3d.com/2020/06/24/package-manager-updates-in-unity-2020-1/?_ga=2.84647326.669754589.1597830146-1414726221.1582037216) - -The NetCode package requires the Entities, Entities Graphics, and Transport packages to work. Entities and the Transport packages are installed automatically through dependencies when installing NetCode, but the Entities Graphics is not. The minimum set of packages you need to manually install are NetCode (com.unity.netcode) and the Entities Graphics (com.unity.entities.graphics). To install these packages while they are still in preview, either edit your project manifest to include the target package name, or type the name of the package you want to install into the **Add package from git URL** menu in the Package Manager. - -For example, to install the Transport package using the Package Manager, go to **Window** > **Package Manager**, click on the plus icon to open the **Add package from...** sub-menu and click on **Add package from git url...**, then type "com.unity.transport" into the text field and press **Enter**. To install the same package through your package.json manifest file, add "com.unity.transport": "0.4.0-preview.1" to your dependencies list. Version 0.4.0-preview.1 is used here as an example and is not a specific version dependency. - -## Create an initial Scene - -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 a new empty Sub Scene via the "New Sub Scene" menu in the Hierarchy context menu (called __SharedData__ in the example). - -![Empty SharedData GameObject](images/world-game-objects.png)
_Empty SharedData Sub Scene_ - -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__ Sub Scene and select __3D Object > Plane__ which then creates a plane that is nested under __SharedData__. - -![Scene with a plane](images/initial-scene.png)
_Scene with a plane_ - -## Create a ghost Prefab - -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**. - -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. Once the prefab is created 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 the Cube Prefab, create a simple component with the following code: - -```c# -using Unity.Entities; -using Unity.NetCode; - -[GenerateAuthoringComponent] -public struct MovableCubeComponent : IComponentData -{ -} -``` - -If you want to add a serialized value to the component, use the __GhostField Attribute__: -```c# -using Unity.Entities; -using Unity.NetCode; - -[GenerateAuthoringComponent] -public struct MovableCubeComponent : IComponentData -{ - [GhostField] - public int ExampleValue; -} -``` - -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. - -Start by adding a __Ghost Owner Component__ and changing the __Default Ghost Mode__ to __Owner Predicted__. The __NetworkId__ member of the __Ghost Owner Component__ needs to be set by 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 which Ghosts to use, you need to reference the prefabs from the sub scene. First we need to create a new component for the spawner with the following code: -```c# -using Unity.Entities; - -[GenerateAuthoringComponent] -public struct CubeSpawner : IComponentData -{ - public Entity Cube; -} -``` - -Right click on SharedData and select __Create Empty__. Rename it to __Spawner__ and then add a __CubeSpawner__. Because both the client and the server need to know about these Ghosts, add it to the __SharedData__ Sub Scene. - -In the Inspector, drag the Cube prefab to the Cube field of the spawner. - -![Ghost Spawner settings](images/ghost-spawner.png)
_Ghost Spawner settings_ - -## Establish a connection -Next, you need to make sure that 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, so write the minimal amount of code to set it up. - -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 - } -} -``` - -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. - -In *Game.cs*, create the following [RpcCommand](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.IRpcCommand.html). This code tells the server that you are ready to start playing. - -```c# -public struct GoInGameRequest : IRpcCommand -{ -} -``` - -To make sure you can send input from the client to the server, you need to create an `ICommandData` struct. 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 -{ - public NetworkTick Tick {get; set;} - public int horizontal; - public int vertical; -} -``` - -The command stream consists of the current tick and the horizontal and vertical movements. The serialization code for the data will be automatically generated. - -To sample the input, send it over the wire. To do this, create a System for it as follows: - -```c# -[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] -public class SampleCubeInput : SystemBase -{ - protected override void OnCreate() - { - RequireForUpdate(); - } - - protected override void OnUpdate() - { - var localInput = GetSingleton().targetEntity; - if (localInput == Entity.Null) - { - var commandBuffer = new EntityCommandBuffer(Allocator.Temp); - var localPlayerId = GetSingleton().Value; - var commandTargetEntity = GetSingletonEntity(); - Entities.WithAll().WithNone().ForEach((Entity ent, in GhostOwnerComponent ghostOwner) => - { - if (ghostOwner.NetworkId == localPlayerId) - { - commandBuffer.AddBuffer(ent); - commandBuffer.SetComponent(commandTargetEntity, new CommandTargetComponent {targetEntity = ent}); - } - }).Run(); - commandBuffer.Playback(EntityManager); - return; - } - var input = default(CubeInput); - input.Tick = GetSingleton().ServerTick; - if (Input.GetKey("a")) - input.horizontal -= 1; - if (Input.GetKey("d")) - input.horizontal += 1; - if (Input.GetKey("s")) - input.vertical -= 1; - if (Input.GetKey("w")) - input.vertical += 1; - var inputBuffer = EntityManager.GetBuffer(localInput); - inputBuffer.AddCommandData(input); - } -} -``` - -Finally, create a system that can read the `CommandData` and move the player. - -```c# -[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] -public class MoveCubeSystem : SystemBase -{ - protected override void OnUpdate() - { - var tick = GetSingleton().ServerTick; - var deltaTime = Time.DeltaTime; - Entities.WithAll().ForEach((DynamicBuffer inputBuffer, ref Translation trans) => - { - CubeInput input; - inputBuffer.GetDataAtTick(tick, out input); - if (input.horizontal > 0) - trans.Value.x += deltaTime; - if (input.horizontal < 0) - trans.Value.x -= deltaTime; - if (input.vertical > 0) - trans.Value.z += deltaTime; - if (input.vertical < 0) - trans.Value.z -= deltaTime; - }).ScheduleParallel(); - } -} -``` - -## Tie it together - -The final step is to create the systems that handle when you enter a 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 -[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] -public class GoInGameClientSystem : SystemBase -{ - protected override void OnCreate() - { - // Make sure we wait with the sub scene containing the prefabs to load before going in-game - RequireForUpdate(); - RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly(), ComponentType.Exclude())); - } - protected override void OnUpdate() - { - var commandBuffer = new EntityCommandBuffer(Allocator.Temp); - Entities.WithNone().ForEach((Entity ent, in NetworkIdComponent id) => - { - commandBuffer.AddComponent(ent); - var req = commandBuffer.CreateEntity(); - commandBuffer.AddComponent(req); - commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent { TargetConnection = ent }); - }).Run(); - commandBuffer.Playback(EntityManager); - } -} -``` - -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 -[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] -public class GoInGameServerSystem : SystemBase -{ - protected override void OnCreate() - { - RequireForUpdate(); - RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly(), ComponentType.ReadOnly())); - } - protected override void OnUpdate() - { - var prefab = GetSingleton().Cube; - var networkIdLookup = GetComponentLookup(true); - var commandBuffer = new EntityCommandBuffer(Allocator.Temp); - Entities.WithNone().ForEach((Entity reqEnt, in GoInGameRequest req, in ReceiveRpcCommandRequestComponent reqSrc) => - { - commandBuffer.AddComponent(reqSrc.SourceConnection); - UnityEngine.Debug.Log(String.Format("Server setting connection {0} to in game", networkIdLookup[reqSrc.SourceConnection].Value)); - var player = commandBuffer.Instantiate(prefab); - commandBuffer.SetComponent(player, new GhostOwnerComponent { NetworkId = networkIdLookup[reqSrc.SourceConnection].Value}); - commandBuffer.AddBuffer(player); - - commandBuffer.SetComponent(reqSrc.SourceConnection, new CommandTargetComponent {targetEntity = player}); - - commandBuffer.DestroyEntity(reqEnt); - }).Run(); - commandBuffer.Playback(EntityManager); - } -} -``` - -## 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 __A,S,D,__ and __W__ keys to move the Cube around. - -If you want to build a standalone player for the project you must create a `BuildConfiguration` - the regular build menu in Unity will not work. - -To recap this workflow: - -1. Create a Sub Scene to hold __SharedData__ between the client and the server. -1. Create a Prefab out of a simple 3D Cube and add a __GhostAuthoringComponent__, a __MovableCubeComponent__ and a __GhostOwnerComponent__. -1. Create a __CubeSpawner__ component and add it to an empty GameObject to create a Spawner. Make sure it is referencing the correct prefab. -1. Establish a connection between the client and the server. -1. Write an `Rpc` to tell the server you are ready to play. -1. Write an `ICommandData` to serialize game input. -1. Write a client system to send an `Rpc`. -1. Write a server system to handle the incoming `Rpc`. +| **Topic** | **Description** | +| :-------------------- | :----------------------- | +| **[Installation](installation.md)** | Installing and Setting up Netcode for Entities | +| **[Networked Cube](networked-cube.md)** | Your first adventure with Netcode for Entities | diff --git a/Documentation~/ghost-snapshots.md b/Documentation~/ghost-snapshots.md index 8499c8d..4c349fa 100644 --- a/Documentation~/ghost-snapshots.md +++ b/Documentation~/ghost-snapshots.md @@ -4,12 +4,14 @@ A ghost is a networked object that the server simulates. During every frame, the 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 Prefab. +## Authoring Ghosts +Ghost can be authored in the editor by creating a Prefab with a [GhostAuthoringComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostAuthoringComponent.html). -![Ghost Authoring Component](images/ghost-config.png)_Ghost Authoring Component_ +![Ghost Authoring Component](images/ghost-config.png) -You must set the __Name__, __Importance__, __Supported Ghost Mode__, __Default Ghost Mode__ and __Optimization Mode__ 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. +The __GhostAuthoringComponent__ has a small editor which you can use to configure how Netcode synchronizes the Prefab.
+You must set the __Name__, __Importance__, __Supported Ghost Mode__, __Default Ghost Mode__ and __Optimization Mode__ property on each ghost.
+Netcode for Entities 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 __Supported Ghost Mode__ types: @@ -28,50 +30,34 @@ You can select from two different __Optimization Mode__ types: * __Dynamic__ - the ghost will be optimized for having small snapshot size both when changing and when not changing. * __Static__ - the ghost will not be optimized for having small snapshot size when changing, but it will not be sent at all when it is not changing. -To override the default client instantiation you can create a classification system updating after __ClientSimulationSystemGroup__ and before [GhostSpawnClassificationSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostSpawnClassificationSystem.html) which goes through the __GhostSpawnBuffer__ buffer on the singleton entity with __GhostSpawnQueueComponent__ and change the __SpawnType__. +## Replicating Components and Buffers +Netcode for Entities uses C# attributes to configure which components and fields are synchronized as part of a ghost. There are two fundamental attributes you can use: +- The [GhostFieldAttribute](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostFieldAttribute.html) +- The [GhostComponentAttribute](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostComponentAttribute.html) -Unity uses attributes in C# to configure which components and fields are synchronized as part of a ghost. You can see the current configuration in the __GhostAuthoringComponent__ by selecting __Update component list__. +The `GhostFieldAttribute` should be used to mark which component (or buffer) fields should be serialised. The attribute can be added to struct fields and properties. Once a component +has at least one field marked with `GhostField`, it become replicated and and transmitted as part of the ghost data. -To change which versions of a Prefab a component is available on you use __PrefabType__ in a __GhostComponentAttribute__ on the component. __PrefabType__ can be on of the these types: -* __InterpolatedClient__ - the component is only available on clients where the ghost is interpolated. -* __PredictedClient__ - the component is only available on clients where the ghost is predicted. -* __Client__ - the component is only available on the clients, both when the ghost is predicted and interpolated. -* __Server__ - the component is only available on the server. -* __AllPredicted__ - the component is only available on the server and on clients where the ghost is predicted. -* __All__ - the component is available on the server and all clients. - -For example, if you add `[GhostComponent(PrefabType=GhostPrefabType.Client)]` to 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. - -A component can set __SendTypeOptimization__ in the __GhostComponentAttribute__ to control which clients the component is sent to whenever a ghost type is known at compile time. The available modes are: -* __None__ - the component is never sent to any clients. NetCode will not modify the component on the clients which do not receive it. -* __Interpolated__ - the component is only sent to clients which are interpolating the ghost. -* __Predicted__ - the component is only sent to clients which are predicting the ghost. -* __All__ - the component is sent to all clients. - -If a component is not sent to a client NetCode will not modify the component on the client which did not receive it. +The `GhostComponentAttribute` should be used to: +- Declare for which version of the Prefab the component should be present. +- Declare if the component should be serialised also for child entities. +- Declare to which subset of clients a component should be replicated. -A component can also set __SendDataForChildEntity__ to true in order to change the default (of not serializing children), allowing this component to be serialized when on a child. - -A component can also set __SendToOwner__ in the __GhostComponentAttribute__ to specify if the component should be sent to client who owns the entity. The available values are: -* __SendToOwner__ - the component is only sent to the client who own the ghost -* __SendToNonOwner__ - the component is sent to all clients except the one who owns the ghost -* __All__ - the component is sent to all clients. - -### Override GhostComponent properties on per prefab basis -It is possible to override the following meta-data on per-prefab basis, via the __GhostAuthoringInspectionComponent__ editor: -* __PrefabType__ -* __SendToOptimization__ -* __Variant__ - -It is possible to prevent a component from supporting per-prefab overrides by using the __DontSupportPrefabOverride__ attribute. When present, the component can't be further customized in the inspector. +## Authoring component serialization +To signal the Netcode for Entities that a component should be serialised you need to add a `GhostField` attribute to the values you want to send. -To prevent a component from supporting per-prefab overrides, add the `[DontSupportPrefabOverride]` attribute to the component type. -Example: The NetCode package requires the __GhostOwnerComponent__ to be added to all ghost types, sent for all ghost types, and serialized using the default variant. Thus, we add the `[DontSupportPrefabOverride]` attribute to it. -When present, the component can't be customized in the inspector, nor can a programmer add custom or default variants for this type (as that will trigger errors during ghost validation). - -### Authoring component serialization -For each component you want to serialize, you need to add an attribute to the values you want to send. Add a `[GhostField]` attribute to the fields you want to send in an `IComponentData`. Both component fields and properties are supported. The following conditions apply in general for a component to support serialization: +```csharp +public struct MySerialisedComponent : IComponentData +{ + [GhostField]public int MyIntField; + [GhostField(Quantization=1000)]public float MyFloatField; + [GhostField(Quantization=1000, Smoothing=SmoothingAction.Interpolate)]public float2 Position; + public float2 NonSerialisedField; + ... +} +``` +The following conditions apply in general for a component to support serialization: * The component must be declared as public. * Only public members are considered. Adding a `[GhostField]` to a private member has no effect. * The __GhostField__ can specify `Quantization` for floating point numbers. The floating point number will be multiplied by this number and converted to an integer in order to save bandwidth. Specifying a `Quantization` is mandatory for floating point numbers and not supported for integer numbers. To send a floating point number unquantized you have to explicitly specify `[GhostField(Quantization=0)]`. @@ -84,38 +70,34 @@ For each component you want to serialize, you need to add an attribute to the va * __GhostField__ `MaxSmoothingDistance` allows you to disable interpolation when the values change more than the specified limit between two snapshots. This is useful for dealing with teleportation for example. * Finally the __GhostField__ has a `SubType` property which can be set to an integer value to use special serialization rules supplied for that specific field. -#### Ghost Field Inheritance - -If a `[GhostField]` is specified for a non primitive field, the attribute and some of its properties are automatically intherithed by all the sub-fields witch does not present a `[GhostField]` attribute. - -```c# +## Authoring dynamic buffer serialization +Dynamic buffers serialization is natively supported. Unlike components, to replicate a buffer **all public fields** MUST be marked with at `[GhostField]` attribute. +>![NOTE] This restriction has been added to guarantee that in case an element is added to the buffer, when it is replicated the element have meaningful values. That restriction may be +> removed in the future. -public struct Vector2 +```csharp +public struct SerialisedBuffer : IBufferElementData { - public float x; - [GhostField(Quantization=100)] public float y; -} - -[GhostComponent] -public struct MyComponent : IComponentData -{ - //Value.x will inherit the quantization value specified by the parent class - //Value.y will maintains its original quantization value - [GhostField(Quantized=1000)] - public Vector Value; + [GhostField]public int Field0; + [GhostField(Quantization=1000)]public float Field1; + [GhostField(Quantization=1000)]public float2 Position; + public float2 NonSerialisedField; //<---- This is an error! + private float2 NonSerialisedField; // private field, because are not replicated, are not set to default and their values are undefined at runtime. + [GhostField(SendData=false)]public int NotSentAndUninitialised; // field that aren't replicated via SendData=false are never set to default and can have any possible value. + ... } ``` -The following properties are not inherited: - -* __SubType__ - the subtype is always reset to the default - -### Authoring dynamic buffer serialization +Furthermore, in line with the `IComponentData`: +* The buffer must be declared as public. +* Only public members are considered. Adding a `[GhostField]` to a private member has no effect. +* By using the `GhostField.SendData` you can instrument the serialisation code to skip certain field. In such a case: + - the value of the fields that aren't replicated are never altered + - for new buffer elements, their content is not set to default and the content is undefined (can be any value). -Dynamic buffers serialization is natively supported. Like components, just add a `[GhostField]` attribute to the fields you want to serialize and the buffer will replicated to all the clients. Use the __GhostComponent__ attribute to specify other serialization behavior. Dynamic buffers fields don't support interpolation. The __GhostField__ `Smoothing` and `MaxSmoothingDistance` properties will be ignored. -### ICommandData and IInputComponentData serialization +## ICommandData and IInputComponentData serialization __ICommandData__, being a subclass of __IBufferElementData__, can also be serialized from server to clients. As such, the same rules for buffers apply: if the command buffer must be serialized, then all fields must be annotated. @@ -139,82 +121,219 @@ The same applies when using automated input synchronization with __IInputCompone The command data serialization is particularly useful for implementing [RemotePlayerPrediction](prediction.md#remote-players-prediction). -## Ghost Component variants, types and serialization +### Ghost Field Inheritance +If a `[GhostField]` is specified for a non primitive field, the attribute and +some of its properties are automatically inherited by all the sub-fields witch does not present a `[GhostField]` attribute. -The types you can serialize via `GhostField` attributes in ghost components are defined via templates. In addition to the default out-of-the-box types supported you can define custom serialization for your own types. You can also define multiple ways to serialize types, via _SubTypes_, and define how 3rd party types you have no control over should be handled, via Ghost Component Variants. See [the custom template types](custom-ghost-types.md) section for more information about how this works. +```c# -## Ghost collection +public struct Vector2 +{ + public float x; + [GhostField(Quantization=100)] public float y; +} -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. +public struct MyComponent : IComponentData +{ + //Value.x will inherit the quantization value specified by the parent class + //Value.y will maintains its original quantization value + [GhostField(Quantized=1000)] public Vector Value; +} +``` -In the Inspector for the __GhostCollectionAuthoringComponent__, there is one button you can select: -* __Update ghost list__, which scans for Prefabs with __GhostAuthoringComponent__. +The following properties are not inherited: +* __SubType__ - the subtype is always reset to the default -For the netcode to work, the ghost collection must be part of the client and server entity worlds. +## Using the GhostComponentAttribute +The `GhostComponentAttribue` **does not indicates or signal** that a component is replicated. Instead, it should be used to instrument the runtime how to handle the component when it comes to: +- Removing the component from a prefab when not necessary +- Optimise sending the component data +- Specify how the component should be handled for child entities. -## Value types +```csharp +[GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.OnlyInterpolatedClients, SendDataForChildEntity=false)] +public struct MyComponent : IComponentData +{ + [GhostField(Quantized=1000)] public float3 Value; +} +``` + +To change which versions of a Prefab a component is available on you use __PrefabType__ in a __GhostComponentAttribute__ on the component. __PrefabType__ can be on of the these types: +* __InterpolatedClient__ - the component is only available on clients where the ghost is interpolated. +* __PredictedClient__ - the component is only available on clients where the ghost is predicted. +* __Client__ - the component is only available on the clients, both when the ghost is predicted and interpolated. +* __Server__ - the component is only available on the server. +* __AllPredicted__ - the component is only available on the server and on clients where the ghost is predicted. +* __All__ - the component is available on the server and all clients. + +For example, if you add `[GhostComponent(PrefabType=GhostPrefabType.Client)]` to 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. + +A component can set __SendTypeOptimization__ in the __GhostComponentAttribute__ to control which clients the component is sent to whenever a ghost type is known at compile time. The available modes are: +* __None__ - the component is never sent to any clients. Netcode will not modify the component on the clients which do not receive it. +* __Interpolated__ - the component is only sent to clients which are interpolating the ghost. +* __Predicted__ - the component is only sent to clients which are predicting the ghost. +* __All__ - the component is sent to all clients. + +A component can also set __SendDataForChildEntity__ to true in order to change the default (of not serializing children), allowing this component to be serialized when on a child. -The codegen does not support all value types, but you can create an assembly with a name ending with `.NetCodeGen`. This assembly should contain a class implementing the interface __IGhostDefaultOverridesModifier__. Implement the method `public void ModifyTypeRegistry(TypeRegistry typeRegistry, string netCodeGenAssemblyPath)` and register additional types in the typeRegistry. The types you register will be used by the code-gen. +A component can also set __SendToOwner__ in the __GhostComponentAttribute__ to specify if the component should be sent to client who owns the entity. The available values are: +* __SendToOwner__ - the component is only sent to the client who own the ghost +* __SendToNonOwner__ - the component is sent to all clients except the one who owns the ghost +* __All__ - the component is sent to all clients. -## Entity spawning +>![NOTE] By setting either the SendTypeOptimisation and/or SendToOwner, to specify to which client the component should be sent will not +> affect the presence of the component on the prefab or modify the component on the client which did not receive it. -When the client side receives a new ghost, the ghost type is determined by a set of classification systems and then a spawn system spawns it. There is no specific spawn message, and when the client receives an unknown ghost ID, it counts as an implicit spawn. +## How to add serialization support for custom types +The types you can serialize via `GhostFieldAttribute` are specified via templates. You can see the default supported types [here](ghost-types-templates.md#Supported Types)
+In addition to the default out-of-the-box types you can also: +- add your own templates for new types. +- provide a custom serialization templates for a types and target by using the _SubTypes_ property of the `GhostFieldAttribute`. -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. +Please check how to [use and write templates](ghost-types-templates.md#Defining additional templates) for more information on the topic. + +## Ghost Component Variants +The [GhostComponentVariationAttribute](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostComponentVariationAttribute.html) is special attribute tha can be used to declare at compile time +a "replication schema" for a type, without the need to markup the fields in the original type, or the original type itself.
+>![NOTE]This new declared type act as proxy from a code-generation perspective. Instead of using the original type, the code-generation system use the declared "variant" to generate a specific +> version of the serialization code. +> ![NOTE] **Ghost components variants for `IBufferElementData` are not fully supported.** + +The `GhostComponentVariationAttribute` has some specific use case in mind: +- Permit to declare 3rd party component for which you don't have direct access (es: in another assembly or dll that does not have Netcode reference) +- Generate multiple serialization version for a type (i.e: encode the position with small and large precision) +- Strip components (i.e: RenderMesh) from certain prefab types (from the Server for example) by overriding or adding a `GhostComponentAttribute` to the type without changing the original declaration. + +```c# + [GhostComponentVariation(typeof(MyComponent))] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.All)] + struct MyComponentVariant + { + [GhostField(Quantization=100, Smoothing=SmoothingAction.Interpolate)] float3 Value; + } +``` -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. +In the example above, the `MyComponentVariant` will generate serialization code for `MyComponent`, using the properties and the attribute present in the variant declaration. -### Implement Predicted Spawning for player spawned objects -The spawn code needs to run on the client, in the client prediction system. Any prefab ghost entity the client instantiates has the __PredictedGhostSpawnRequestComponent__ added to it and is therefore treated as a predict spawned entity by default. 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. +The attribute constructor take as argument the type of component you want to specify the variant for (ex: `MyComponent`). Then for each field in the original struct you would like to serialize you +should add a `GhostField` attribute like you usually do. +>~[NOTE] Only members that are present in the component type are allowed. Validation and exceptions are thrown at compile time in case the rule is not respected. -These client spawned objects are automatically handled unless a custom classification system is implemented to handle that ghost type. The default system matches ghost types with a spawn tick within 5 ticks of new ghosts found in the ghost snapshot data. You can implement a custom classification with more advanced logic than this. To do that you create a system updating in the __ClientSimulationSystemGroup__ after [GhostSpawnClassificationSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostSpawnClassificationSystem.html). The classification system needs to go through the __GhostSpawnBuffer__ buffer stored on a singleton with a __GhostSpawnQueueComponent__. For each entry in that list it should compare to the entries in the __PredictedGhostSpawn__ buffer on the singleton with a __PredictedGhostSpawnList__ component. If the two entries are the same the classification system should set the __PredictedSpawnEntity__ property in the __GhostSpawnBuffer__ and remove the entry from __GhostSpawnBuffer__. +An optional `GhostComponentAttribute` attribute can be added to the variant to further specify the component serialization properties. -NetCode spawns entities on clients when there is a Prefab available for it. Pre spawned ghosts will work without any special consideration since they are referenced in a sub scene, but for manually spawned entities you must make sure that the prefabs exist on the client. You make sure that happens by having a component in a scene which references the prefab you want to spawn. +It is possible to declare multiple serialization variant for a component (ex: a 2D rotation that just serialize the angle instead of a full quaternion). -## Prespawned ghosts +### Preventing a component to support variations +There are cases when you to prevent a component for supporting variation. (i.e builtin components that have carefully designed to work that way). -A ghost instance (an instance of a ghost prefab) can be placed in a subscene in the Unity editor so that it will be treated just like a normal spawned ghost when the player has loaded the data. There are two restrictions for prespwaned ghosts. Firstly, it must be an instance of a ghost prefab which has been registered in the ghost collection. Secondly, it must be place in a subscene. +It is possible to prevent a component from supporting variation by using the [DontSupportPrefabOverridesAttribute](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.DontSupportPrefabOverridesAttribute.html) attribute. +An error will be reported at compile time if a `GhostComponentVariation` is defined for that type. -The ghost authoring component on the prespawned ghost cannot be configured differently than the ghost prefab source, since that data is handled on a ghost type basis. +### Specify which variant to use on a Ghost Prefab. +Using `GhostAuthoringInspectionComponent` it is then possible to select for each prefab what serialization variants to for each individual components. -Each subscene applies prespawn IDs to the ghosts it contains in a deterministic manner. The subscene hashes the component data on the ghosts, which currently is only the `Rotation` and `Translation` components. It also keeps a single hash composed of all the ghost data for the subscene itself. +![Ghost Authoring Variants](images/ghost-inspection.png) -At runtime, when all subscenes have been loaded, there is a process which applies the prespawn ghost IDs to the ghosts as normal runtime ghost IDs. This has to be done after all subscenes have finished loading and the game is ready to start. It is also done deterministically, so that for each player (server or client), the ghost IDs are applied in exactly the same way. This happens when the `NetworkStreamInGame` component has been added to the network connection. Currently, there is no reliable builtin way to detect when subscenes have been loaded. However, it's possible to do so manually. To do this, add a custom tag to every subscene, then count the number of tags to detect when all subscenes are ready. +All variants for that specific component type present in the project will be show in a dropbox selection.
+You can assign and use different variant for the GameObject (and so baked entity) in you hierarchy. -An alternative way to detect whether subscenes have finished loading without using tags is to check if the prespawn ghost count is correct. The following example shows one possible solution for checking this number, in this case testing for 7 ghosts across all loaded subscenes: +### Assign default variant to use for a type. +In cases where multiple variants are present for a type that does not have a "default" serialization (that it, the type we are specifying the variation for does not have any ghost fields) +is considered a conflict. We use some built-in rule to retrieve a deterministic variant to use but, in general, __it is the users responsibility__ to indicate in this case what type should be the default. + +To setup which variant to use as the `default` for a given type you need to create a system that inherit from +[DefaultVariantSystemBase](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.DefaultVariantSystemBase.html) class, +and implements the `RegisterDefaultVariants` method. ```c# -[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] -public class GoInGameClientSystem : ComponentSystem +using System.Collections.Generic; +using Unity.Entities; +using Unity.Transforms; + +namespace Unity.NetCode.Samples +{ + sealed class DefaultVariantSystem : DefaultVariantSystemBase + { + protected override void RegisterDefaultVariants(Dictionary defaultVariants) + { + defaultVariants.Add(typeof(LocalTransform), Rule.OnlyParents(typeof(TransformDefaultVariant))); + } + } +} +``` + +This class would make sure the default `LocalTransform` variant to us as default is the `TransformDefaultVariant`. For more information, please refer to the +[DefaultVariantSystemBase](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.DefaultVariantSystemBase.html) documentation. + +## Special variant types + +There might be cases where you need the variant to remove functionality instead of doing things differently and there are two cases covered. This saves the typing involved with creating a full variant registration which essentially is just turning it off. + +When you want a component to be stripped on clients so you don't see it at all there you can use the `ClientOnlyVariant` type when registering the default variant for a particular type. + +When you don't want any synchronization to be done with a variant type, so no serialization happens, you can use the `DontSerializeVariant` type when registering. + +```C# +using System.Collections.Generic; +using Unity.Entities; +using Unity.Transforms; + +namespace Unity.NetCode.Samples { - public int ExpectedGhostCount = 7; - protected override void OnUpdate() + sealed class DefaultVariantSystem : DefaultVariantSystemBase { - var prespawnCount = EntityManager.CreateEntityQuery(ComponentType.ReadOnly()).CalculateEntityCount(); - Entities.WithNone().ForEach((Entity ent, ref NetworkIdComponent id) => + protected override void RegisterDefaultVariants(Dictionary defaultVariants) { - if (ExpectedGhostCount == prespawnCount) - PostUpdateCommands.AddComponent(ent); - }); + defaultVariants.Add(typeof(SomeClientOnlyThing), Rule.OnlyParents(typeof(ClientOnlyVariant))); + defaultVariants.Add(typeof(NoNeedToSyncThis), Rule.ForAll(typeof(DontSerializeVariant))); + } } } ``` -To create a prespawned ghost from a normal scene you can do the following: -* Right click on the *Hierarchy* in the inspector and click *New Sub Scene*. -* Drag an instance of a ghost prefab into the newly created subscene. +You can also pick the `DontSerializeVariant` in the ghost component on prefabs. -This feature is new and is liable to change in the future. The current implementation has some limitations which are listed below: -* With regard to using subscenes, when placing an object in a subscene, you no longer place the `ConvertToClientServerEntity` component on it as being in a subscene implies conversion to an Entity. Also, it means the option of making an entity only appear on the client or server is now missing. Prespawned ghosts always appear on both client and server as they are just like a normal spawned ghost, and will always be synchronized (as configured) after the game starts. -* Loading a new subscene with prespawned ghosts after starting (entering the game) is currently not supported. -* Only the `Translation` and `Rotation` `IComponentData` components, converted from the `Transform` component, are currently used to generate the prespawn IDs. This means that the prespawn ghosts cannot be placed in the same location and these components are required to use prespawn ghosts. -* If prespawned ghosts are moved before going in game the baseline data will not be calculated properly for it which will result in the snapshot delta compression failing. This data is validated when clients connect and will cause a disconnect. **Prespawned ghosts should only be moved after going in game**. +## Assign variants and override GhostComponentAttribute settings on ghost prefabs +It is possible to override the following meta-data on per-prefab basis, +by using the [GhostAuthoringInspectionComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.AutoCommandTarget.html) editor. + +![Ghost Authoring Component](images/ghost-inspection.png) + +The `GhostAuthoringInspectionComponent` should be added to the `GameObject` you would like to customise. Once added, the editor will show which components present in the runtime entity are replicated.
+The editor allow you to: change the following properties: + +* Change the __PrefabType__ in which the component should be present/replicated. +* Change the __SendToOptimization__ for this component (if applicable) +* Assign the serialization __Variant__ to use for that component. + +It is possible to prevent a component from supporting per-prefab overrides by using the [DontSupportPrefabOverrides](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.DontSupportPrefabOverridesAttribute.html) +attribute.
+When present, the component can't be customized in the inspector, nor can a programmer add custom or default variants for this type (as that will trigger errors during ghost validation). + +For example: The Netcode for Entities package requires the [GhostOwnerComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostOwnerComponent.html) +to be added to all ghost types, sent for all ghost types, and serialized using the default variant. Thus, we add the `[DontSupportPrefabOverride]` attribute to it. + +>![NOTE] Components on child entities are not serialised by default, thus by default when you look to `GhostAuthoringInspectionComponent` on a child GameObject you will +> see that the selected variant for the type is the `DontSerializeVariant`. + +DontSerializeVariant ## 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. +To understand what is being put on the wire in the Netcode, you can use the snapshot visualization tool, __NetDbg__ tool. + +net debug tool + +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 received snapshot, with a breakdown of the snapshot’s ghost types, size etc. + +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. + +## Ghosts vs. RPCs + +RPCs are mostly meant for game flow events, like making everyone do a certain thing like load a level. Opposed to that ghost snapshot synchronization is meant to make sure certain data is always replicated and kept in sync with everyone with certain parameters. Some differences are: + +* RPCs are sent as reliable packets, while ghosts snapshots are unreliable. +* RPC data is sent and received as it is, while ghost data goes through optimizations like diff compression and can go through value smoothing when received. +* RPCs are not tied to any particular tick or other snapshot data (just processed when received). Ghost snapshot data can work with interpolation and prediction (with snapshot history) resulting in getting applied at particular ticks. diff --git a/Documentation~/ghost-spawning.md b/Documentation~/ghost-spawning.md new file mode 100644 index 0000000..425ceb5 --- /dev/null +++ b/Documentation~/ghost-spawning.md @@ -0,0 +1,153 @@ +# Spawning Ghost Entities + +A ghost is spawned simply by instantiating it on the server, all ghosts on the server are replicated to all clients. Ghosts which are inside subscenes are a special case and handled as pre-spawned ghosts. If their subscene state has not been changed when a client connects he does not need any snapshot data updates for pre-spawned ghosts. On clients ghosts can be predict spawned and later matched with a server spawn as soon as a snapshot update is applied for it. It is not valid to instantiate another type of ghosts on clients as the server is authoritative and is the true authority of what ghosts exists in the world. + +## Ghosts Entities on the client +Netcode for Entities does not have and nor does requires a specific spawn message. When the client receives an unknown/new ghost id, it counts as an implicit spawn. + +When the client receives a new ghost, the ghost is first "classified" by a set of **classification systems** to determine its **spawning type**. +Once the `spawn type` has been set, the [GhostSpawnSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostSpawnSystem.html) will take care of instantiating the new entity. + +### Different type of spawning +Spawning is split into three main types as follows: +* __Delayed or interpolated spawning__: Because the client interpolates snapshot data, non predicted ghosts cannot be immediately spawned. Otherwise, the object would appear and then not get any more updates until we receive new data from server and we can start interpolating it. +
This spawn delay is governed by the `Interpolation Delay`. The interpolated ghosts spawn when the `Interpolated Tick` is greater or equals their spawn tick. See [time synchronization](time-synchronization.md) for more information about interpolation delay and interpolation tick. +* __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. When 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__: This usually applies to objects that the player spawns, like in-game bullets or rockets that the player fires. + +>![NOTE] Ghost entities can be spawned only if the ghost prefabs are loaded/present in the world. Server and client should agree upon the prefabs they have and the server will only report to the client +> ghosts for which the client has the prefab. + +### Implement Predicted Spawning for player spawned objects +The spawn code needs to run on the client, in the client prediction system.
+Any prefab ghost entity the client instantiates has the __PredictedGhostSpawnRequestComponent__ added to it and is therefore treated as a predict spawned entity by default. + +When the first snapshot update for this entity arrives, we detect that the received update is for an entity already spawned by client and from that time on, all the updates will be applied to it. + +In the prediction system code the [NetworkTime.IsFirstTimeFullyPredictingTick](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.NetworkTime.html) value needs to be checked in order to prevent the spawned object from being spawned multiple times as data is rolled back and redeployed as part of the prediction loop. + +```csharp +public void OnUpdate() +{ + // Other input like movement handled here or in another system... + + var networkTime = SystemAPI.GetSingleton(); + if (!networkTime.IsFirstTimeFullyPredictingTick) + return; + // Handle the input for instantiating a bullet for example here + // ... +} +``` + +These client spawned objects are automatically handled by the [GhostSpawnClassificationSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostSpawnClassificationSystem.html) system, +that matches the new received ghosts with any of the client predicted spawned ones, based by their types and spawning tick (should be within 5 ticks). + +You can implement a custom classification with more advanced logic than this to override the default behaviour. + +#### Adding your own classification system +To override the default client classification you can create your own classification system. The system is required to: +- Update in the `GhostSimulationSystemGroup` +- Run after the `GhostSpawnClassificationSystem` + +The classification system can inspect the ghosts that need to be spawned by retrieving the +[GhostSpawnBuffer](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostSpawnBuffer.html) buffer on the singleton +[GhostSpawnQueueComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostSpawnQueueComponent.html) entity and change their __SpawnType__. + +Each entry in that list should be compared to the entries in the `PredictedGhostSpawn` buffer on the singleton with a `PredictedGhostSpawnList` component. +If the two entries have the same type and "match", the classification system should set the __PredictedSpawnEntity__ property in the `GhostSpawnBuffer` element and remove the entry from `PredictedGhostSpawn` buffer. + +```csharp +public void Execute(DynamicBuffer ghosts, DynamicBuffer data) +{ + var predictedSpawnList = PredictedSpawnListLookup[spawnListEntity]; + for (int i = 0; i < ghosts.Length; ++i) + { + var newGhostSpawn = ghosts[i]; + if (newGhostSpawn.SpawnType != GhostSpawnBuffer.Type.Predicted || newGhostSpawn.HasClassifiedPredictedSpawn || newGhostSpawn.PredictedSpawnEntity != Entity.Null) + continue; + + // Mark all the spawns of this type as classified even if not our own predicted spawns + // otherwise spawns from other players might be picked up by the default classification system when + // it runs. + if (newGhostSpawn.GhostType == ghostType) + newGhostSpawn.HasClassifiedPredictedSpawn = true; + + // Find new ghost spawns (from ghost snapshot) which match the predict spawned ghost type handled by + // this classification system. You can use the SnapshotDataBufferComponentLookup to inspect components in the + // received snapshot in your matching function + for (int j = 0; j < predictedSpawnList.Length; ++j) + { + if (newGhostSpawn.GhostType != predictedSpawnList[j].ghostType) + continue; + + if (YOUR_FUZZY_MATCH(newGhostSpawn, predictedSpawnList[j])) + { + newGhostSpawn.PredictedSpawnEntity = predictedSpawnList[j].entity; + predictedSpawnList[j] = predictedSpawnList[predictedSpawnList.Length - 1]; + predictedSpawnList.RemoveAt(predictedSpawnList.Length - 1); + break; + } + } + ghosts[i] = newGhostSpawn; + } +} +``` + +Inside your classification system you can use the [SnapshotDataBufferComponentLookup](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.GhostSpawnQueueComponent.html) to: +- Check for component presence in the ghost archetype +- Retrieve from the snapshot data associated with the new ghost any replicated component type. + +## Pre-spawned ghosts +A ghost instance (an instance of a ghost prefab) can be placed in a sub-scene in the Unity editor so that it will be treated just like a normal spawned ghost when the scene is loaded. + +To create a prespawned ghost from a normal scene you can do the following: +* Right click on the *Hierarchy* in the inspector and click *New Sub Scene*. +* Drag an instance of a ghost prefab into the newly created subscene. + +prespawn ghost + +There are some restrictions for pre-spawned ghosts: +- **It must be an instance of a ghost prefab**. +- **It must be place in a sub-scene**. +- **Pre-spawned ghosts in the same scene cannot have the exact same position and rotation**. +- **Pre-spawned ghosts MUST be put always put on the main scene section (section 0).** +- The ghost authoring component on the pre-spawned ghost cannot be configured differently than the ghost prefab source (that data is handled on a ghost type basis). + +### How pre-spawned ghosts works +At baking time, each sub-scene assign a `PrespawnId` to the ghosts it contains in a deterministic manner. The ids are assigned by sorting the ghost by the mean of a deterministic hash that takes in account the `Rotation` and `Translation` of the entity. + +For each sub-scene then, a combined hash that contains: +- the SceneGUID +- all the ghosts calculated hashes + +is extracted and used to : +- group the pre-spawned ghost on a per-sub-scene basis by assigning a `SubSceneGhostComponentHash` shared component to all the ghosts in the scene +- add to the `SceneSection` a `SubSceneWithPrespawnGhosts` component, that will be used by the runtime to handle sub-scene with pre-spawned ghosts. + +At runtime, when a sub-scene has been loaded, is processed by both client and server: +- For each pre-spawned ghost, a `Prespawn Baseline` is extracted and used to delta compress the ghost component when it is first sent (bandwidth optimisation) +- The server assign to sub-scene a unique `Ghost Id Range` that is used to assign distinct ghost-id to the pre-spawned ghosts based on their `PrespawnId`. +- The server will replicated to the client by using an internal ghost entity, the assigned id ranges for each sub-scene (identified by the hash assigned to the `SubSceneWithPrespawnGhosts` component) +- Once the client has loaded the sub-scene and received the ghost range, it will then: + - Assign to the pre-spawned ghosts the server authoritative ids + - Report to the server it is ready to stream the pre-spawned ghosts (via rpc) + +This has to be done after all sub-scene have finished loading and the connection (at least one for the server) has bee set as `in game` (when the `NetworkStreamInGame` component has been added to the network connection). + +>![NOTE] If prespawned ghosts are moved before going in game or in general before the baseline is calculated properly, data may be not replicated correctly (the snapshot delta compression will fail). +> Both server and client calculate a CRC of the baseline and this hash is validated when clients connect. A mismatch will cause a disconnection. This also the reason why the ghost are `Disabled`. + +For both client and server, when a sub-scene has been processed (ghost id assigned) a [PrespawnsSceneInitialized](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientTrackLoadedPrespawnSections.html) +component is added to the main `SceneSection`.
+The client automatically track when sub-scene with pre-spawned ghosts are loaded/unloaded and report to the server to stop streaming pre-spawned ghosts associated with them. + +#### Dynamic loading sub-scene with pre-spawned ghosts. +It is possible to load at runtime a sub-scene with pre-spawned ghosts while you are already `in-game`. The pre-spawned ghosts will be automatically handled and synchronised. It also possible to unload sub-scenes that +contains pre-spawned ghosts on demand. Netcode for Entities will handle that automatically, and the server will stop reporting the pre-spawned ghosts for sections the client has unloaded. + +>![NOTE] Pre-spawned ghost when baked become `Disabled` (the `Disable` tag is added to the entity at baking time). The entity is re-enabled after the scene is loaded and the serialisation baseline has been calculated. + +You can get more information about the pre-spawned ghost synchronization flow, by checkin the: +- [ClientPopulatePrespawnedGhostsSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientPopulatePrespawnedGhostsSystem.html) +- [ServerPopulatePrespawnedGhostsSystem](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientPopulatePrespawnedGhostsSystem.html) +- [ClientTrackLoadedPrespawnSections](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.ClientTrackLoadedPrespawnSections.html) diff --git a/Documentation~/ghost-types-templates.md b/Documentation~/ghost-types-templates.md index 7f2ce98..2508133 100644 --- a/Documentation~/ghost-types-templates.md +++ b/Documentation~/ghost-types-templates.md @@ -1,7 +1,9 @@ -# Ghost component types and variants +# Ghost Templates -Ghost components and ghost field types are all handled a certain way during conversion and code generation to produce the right code when building players. It's possible to define the desired behavior in code and on a per ghost prefab basis. +Ghost components and ghost field types are all handled a certain way during conversion and code generation to produce the right code when building players. +It's possible to define the desired behavior in code and on a per ghost prefab basis. +## Supported Types Inside the package we have default templates for how to generate serializers for a limited set of types: * bool @@ -21,23 +23,28 @@ Inside the package we have default templates for how to generate serializers for * ushort * int * uint +* long +* ulong * enums (only for int/uint underlying type) -* Quaternion +* quaternion +* double -Each template can also define different ways to serialize (see also ghost-snapshots.md#Authoring-component-serialization) +For certain types (i.e float, double, quaternion ,float2/3/4) multiple templates exists to handle different way to serialise the type: -* Quantized or unquantized. Where quantized means a float value is sent as an int with a certain multiplication factor which sets the precision (12.456789 can be sent as 12345 with a quantization factor of 1000). +* Quantized or un-quantized. Where quantized means a float value is sent as an int with a certain multiplication factor which sets the precision (12.456789 can be sent as 12345 with a quantization factor of 1000). * Smoothing method as clamp or interpolate/extrapolate. Meaning the value can be applied from a snapshot as interpolated/extrapolated or unmodified directly (clamped). -Since each of these can change how the source value is serialized and then unserialized and applied on the target these might new templates or a region inside defined to handle certain cases (like how to interpolate the value). +Since each of these can change how the source value is serialized, deserialized and applied on the target -There are two ways for customizing how these are handled. Ghost component *variants* and *subtypes*. Variants can allow you to define another way for example to synchronize a float, define a different way to quantize it for example. +these might new templates or a region inside defined to handle certain cases (like how to interpolate the value). + +Ghost component *variants* and *subtypes*. Variants can allow you to define another way for example to synchronize a float, define a different way to quantize it for example. ## Defining additional templates It's possible to register other types which are not supported and the default templates either don't cover at all or need separate handling. -Templates are added to the project by implementing a partial class, **UserDefinedTemplates**, and injecting it into the `Unity.NetCode` package by using +Templates are added to the project by implementing a partial class, **UserDefinedTemplates**, and injecting it into the `Unity.Netcode` package by using an [AssemblyDefinitionReference](https://docs.unity3d.com/2020.1/Documentation/Manual/class-AssemblyDefinitionReferenceImporter.html). The partial implementation must define the method `RegisterTemplates` and add new `TypeRegistry` entries. @@ -65,7 +72,7 @@ namespace Unity.NetCode.Generators } ``` -The template _MySpecialTypeTemplate.cs_ needs to be set up similar to default types, here is the default Float template (where the float is quantized and stored in an int): +The template _MySpecialTypeTemplate.cs_ needs to be set up similarly to default types, here is the default Float template (where the float is quantized and stored in an int): ```c# #region __GHOST_IMPORTS__ @@ -174,19 +181,16 @@ namespace Generated } ``` -When `Quantized` is set to true the *\_\_GHOST_QUANTIZE_SCALE\_\_* variable must be present in the template, and also the quantized scale must be specified when using the type in a `GhostField` +When `Quantized` is set to true the *\_\_GHOST_QUANTIZE_SCALE\_\_* variable must be present in the template, and also the quantization scale **must** be specified when using the type in a `GhostField` `Smoothing` is also important as it changes how serialization is done in the _CopyFromSnapshot_ function, all sections must be filled in. `TemplateOverride` is used when you want to re-use an existing template but only override a specific section of it. This works well when using `Composite` types as you'll point `Template` to the basic type (like float template) and the `TemplateOverride` to only the sections which need to be customized. For example float2 only defines _CopyFromSnapshot_, _ReportPredictionErrors_ and _GetPredictionErrorNames_, the rest uses the basic float template as a composite of the 2 values float2 contains. ---- -**NOTE**: It's important that the templates (when using .cs extension) are in a folder with an .asmdef effectively disabling compilation on it, since this isn't real code we want compiled. It can be done by adding an invalid conditional define on the .asmdef (we use *NETCODE_CODEGEN_TEMPLATES* define in the samples). It's possible though to just store them with any extension (like .txt) and then the compiler won't consider them. ---- -**NOTE**: When making changes to the templates you need to use the _Multiplayer->Force Code Generation_ menu to force a new code compilation which will use the updated templates. +>![NOTE]: It's important that the templates (when using .cs extension) are in a folder with an .asmdef effectively disabling compilation on it, since this isn't real code we want compiled. It can be done by adding an invalid conditional define on the .asmdef (we use *NETCODE_CODEGEN_TEMPLATES* define in the samples). It's possible though to just store them with any extension (like .txt) and then the compiler won't consider them. ---- +>![NOTE]: When making changes to the templates you need to use the _Multiplayer->Force Code Generation_ menu to force a new code compilation which will use the updated templates. ## Defining SubTypes and templates @@ -203,9 +207,9 @@ public struct MyComponent : Unity.Entities.IComponentData ``` -SubTypes are added to projects by implementing a partial class, **GhostFieldSubTypes**, and injecting it into the `Unity.NetCode` package by using +SubTypes are added to projects by implementing a partial class, **GhostFieldSubTypes**, and injecting it into the `Unity.Netcode` package by using an [AssemblyDefinitionReference](https://docs.unity3d.com/2020.1/Documentation/Manual/class-AssemblyDefinitionReferenceImporter.html). The implementation should just -need to add new constant literals to that class (at your own discretion) and they will be available to all your packages which already reference the `Unity.NetCode` assembly. +need to add new constant literals to that class (at your own discretion) and they will be available to all your packages which already reference the `Unity.Netcode` assembly. ```c# namespace Unity.NetCode @@ -217,7 +221,7 @@ namespace Unity.NetCode } ``` -Templates for the subtypes are handled like normal user defined templates but need to set the subtype index. So they are added to the project by implementing the partial class, **UserDefinedTemplates**, and injecting it into the Unity.NetCode package by using +Templates for the subtypes are handled like normal user defined templates but need to set the subtype index. So they are added to the project by implementing the partial class, **UserDefinedTemplates**, and injecting it into the Unity.Netcode package by using an [AssemblyDefinitionReference](https://docs.unity3d.com/2020.1/Documentation/Manual/class-AssemblyDefinitionReferenceImporter.html). The partial implementation must define the method `RegisterTemplates` and add new`TypeRegistry` entries. @@ -251,100 +255,3 @@ As when using any template registration like this, you need to be careful to spe --- **IMPORTANT**: The `Composite` parameter should always be false with subtypes as it is assumed the template given is the one to use for the whole type. - ---- - -## Ghost Component Variants - -To add networking serialization capability to a type that does not have ghostfields and for which you don't -have access to (or cannot be modified), you must create a ghost component variant using the `[GhostComponentVariation]` attribute. - -The attribute constructor take as argument the type of component you want to specify the variant for (ex: Rotation). Then for each field in the original struct you would like to serialize you should add a __GhostField__ attribute like you usually do. Only members that are present in the component type are allowed. Validation and exceptions are thrown at compile time in case the rule is not respected. -At the moment it is __mandatory to add and annotate all the fields for a variant__. - -An example of a variant would be: - -```c# - [GhostComponentVariation(typeof(Transforms.Translation))] - [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.All)] - public struct TranslationVariant - { - [GhostField(Composite=true, Quantization=100, Interpolate=true)] public float3 Value; - } -``` - -In this case the `TranslationVariant` will generate serialization code for `Transforms.Translation`, using the properties and the attribute present in the variant declaration. - -A `GhostComponentAttribute` attribute can be added to the variant to further specify the component serialization properties. - -It is possible to prevent a component from supporting variation by using the `DontSupportVariation` attribute. When present, if a `GhostComponentVariation` is defined for that type, an exception is triggered. - -Ghost components variants for `IBufferElementData` is not fully supported. - -### How to specify what variant to use -Using `GhostAuthoringComponentEditor` it is then possible to select for each prefab what serialization variants to for each individual components. - -![Ghost Authoring Variants](images/variants.png) - -### Handling multiple ghost variants - -It is possible to configure different serialization variant for a component which already has a variant (ex: a 2D rotation that just serialize the angle instead of a full quaternion). - -In these cases where multiple variant are present for a type that does not have a "default" serialization (that it, the type we are specifying the variation for does not have any ghost fields) is considered a conflict. We can't in fact discern any more what it is the correct serialization to use for that type. To solve the conflicts, netcode use the first serializer in hash order. __It is the users responsibility__ to indicate in this case what type should be the default. - -By using a singleton `GhostVariantAssignmentCollection` component you can control what variant to use by default for any type. The singleton must be created for both client and server worlds. Some utility methods are provided to make the assignment easier. - -The method of registering which variants are the defaults for each type is by declaring the `RegisterDefaultVariants` function in an implementation of the `DefaultVariantSystemBase` class. - -```c# -using System.Collections.Generic; -using Unity.Entities; -using Unity.Transforms; - -namespace Unity.NetCode.Samples -{ - sealed class DefaultVariantSystem : DefaultVariantSystemBase - { - protected override void RegisterDefaultVariants(Dictionary defaultVariants) - { - defaultVariants.Add(typeof(Rotation), Rule.OnlyParents(typeof(RotationDefault))); - defaultVariants.Add(typeof(Translation), Rule.OnlyParents(typeof(TranslationDefault))); - } - } -} -``` - -This class would make sure the default `Translation` and `Rotation` variants which come with the package are set as the defaults. - -## Special variant types - -There might be cases where you need the variant to remove functionality instead of doing things differently and there are two cases covered. This saves the typing involved with creating a full variant registration which essentially is just turning it off. - -When you want a component to be stripped on clients so you don't see it at all there you can use the `ClientOnlyVariant` type when registering the default variant for a particular type. - -When you don't want any synchronization to be done with a variant type, so no serialization happens, you can use the `DontSerializeVariant` type when registering. - -```C# -using System.Collections.Generic; -using Unity.Entities; -using Unity.Transforms; - -namespace Unity.NetCode.Samples -{ - sealed class DefaultVariantSystem : DefaultVariantSystemBase - { - protected override void RegisterDefaultVariants(Dictionary defaultVariants) - { - defaultVariants.Add(typeof(SomeClientOnlyThing), Rule.OnlyParents(typeof(ClientOnlyVariant))); - defaultVariants.Add(typeof(NoNeedToSyncThis), Rule.ForAll(typeof(DontSerializeVariant))); - } - } -} -``` - -You can also pick the `DontSerializeVariant` in the ghost component on prefabs. -Reminder: Components on child entities default to `DontSerializeVariant`. Thus: `Rule.ForAll(typeof(DontSerializeVariant))` could also be `Rule.OnlyParents(typeof(DontSerializeVariant))` (although this way is more explicit). - -TODO: Update images for 1.0. - -![DontSerializeVariant](images/dontserialize-variant.png) diff --git a/Documentation~/images/create_subscene.png b/Documentation~/images/create_subscene.png new file mode 100644 index 0000000..c532777 Binary files /dev/null and b/Documentation~/images/create_subscene.png differ diff --git a/Documentation~/images/dontserialize-variant.png b/Documentation~/images/dontserialize-variant.png index 72858b8..7bcd665 100644 Binary files a/Documentation~/images/dontserialize-variant.png and b/Documentation~/images/dontserialize-variant.png differ diff --git a/Documentation~/images/enable-autocommand.png b/Documentation~/images/enable-autocommand.png new file mode 100644 index 0000000..495ebbb Binary files /dev/null and b/Documentation~/images/enable-autocommand.png differ diff --git a/Documentation~/images/ghost-config.png b/Documentation~/images/ghost-config.png index 3e228f1..d1135d9 100644 Binary files a/Documentation~/images/ghost-config.png and b/Documentation~/images/ghost-config.png differ diff --git a/Documentation~/images/ghost-inspection.png b/Documentation~/images/ghost-inspection.png new file mode 100644 index 0000000..a08eb8d Binary files /dev/null and b/Documentation~/images/ghost-inspection.png differ diff --git a/Documentation~/images/ghost-spawner.png b/Documentation~/images/ghost-spawner.png index c5834eb..d6aa0bb 100644 Binary files a/Documentation~/images/ghost-spawner.png and b/Documentation~/images/ghost-spawner.png differ diff --git a/Documentation~/images/hierarchy-view.png b/Documentation~/images/hierarchy-view.png new file mode 100644 index 0000000..f43a4dd Binary files /dev/null and b/Documentation~/images/hierarchy-view.png differ diff --git a/Documentation~/images/initial-scene.png b/Documentation~/images/initial-scene.png index 34b15c2..edea045 100644 Binary files a/Documentation~/images/initial-scene.png and b/Documentation~/images/initial-scene.png differ diff --git a/Documentation~/images/playmode-tool.png b/Documentation~/images/playmode-tool.png new file mode 100644 index 0000000..6d441b9 Binary files /dev/null and b/Documentation~/images/playmode-tool.png differ diff --git a/Documentation~/images/playmode-tools.png b/Documentation~/images/playmode-tools.png deleted file mode 100644 index 914f5b3..0000000 Binary files a/Documentation~/images/playmode-tools.png and /dev/null differ diff --git a/Documentation~/images/prespawn-ghost.png b/Documentation~/images/prespawn-ghost.png new file mode 100644 index 0000000..e0823c6 Binary files /dev/null and b/Documentation~/images/prespawn-ghost.png differ diff --git a/Documentation~/images/replicated-cube.png b/Documentation~/images/replicated-cube.png new file mode 100644 index 0000000..48d27bf Binary files /dev/null and b/Documentation~/images/replicated-cube.png differ diff --git a/Documentation~/images/snapshot-debugger.png b/Documentation~/images/snapshot-debugger.png new file mode 100644 index 0000000..088f5f4 Binary files /dev/null and b/Documentation~/images/snapshot-debugger.png differ diff --git a/Documentation~/index.md b/Documentation~/index.md index 8c5098b..f05c29a 100644 --- a/Documentation~/index.md +++ b/Documentation~/index.md @@ -1,24 +1,27 @@ -# 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. +# Unity Netcode for Entities +The Netcode for Entities 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. ## 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. ### 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](https://github.com/Unity-Technologies/DOTSSample) package also uses the NetCode package to get a more realistic test. +The Netcode for Entities developers are prototyping the package in a simple multi-directional shooter game, similar to Asteroids, as well as another set of samples, publicly available [here](https://github.com/Unity-Technologies/multiplayer), which are used for both showcasing the package features and testing the package internally. -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/). +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 Netcode Forum](https://forum.unity.com/forums/dots-netcode.425/). ## 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. + +To install this package, follow the [installation](installation.md) instructions. ## Requirements -This version of Unity NetCode is compatible with the following versions of the Unity Editor: -* 2020.2.4f1-dots-5 and later (recommended) [Get From Here](unityhub://2020.2.4f1-dots.5/01acd19d2e17) +Netcode for Entities requires you to have Unity version __2022.2.0b8__ or higher. 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. ## Known issues -* Modifying a variant in the `GhostAuthoringInspectionComponent` back to the default variant throws the following error: `InvalidOperationException: Sequence contains no matching element`. To fix this, remove the override via the right click menu, or by setting the `VariantHash` to 0 in the prefab YAML. \ No newline at end of file +* Modifying a variant in the `GhostAuthoringInspectionComponent` back to the default variant throws the following error: `InvalidOperationException: Sequence contains no matching element`. To fix this, remove the override via the right click menu, or by setting the `VariantHash` to 0 in the prefab YAML. +* Making IL2CPP build with code stripping set low or higher crashes the player (missing constructors). Code stripping must be always set to none/minimal. +* When connecting to a server build with the editor as a client (like from frontend menu), make sure the auto-connect ip/port fields in the playmode tools are empty, if not it will get confused and create two connections to the server. +* When using Built-in build, after switching from a Dedicated Server build to a normal Standalone Player (via) the define UNITY_SERVER is not removed from the project. diff --git a/Documentation~/installation.md b/Documentation~/installation.md new file mode 100644 index 0000000..3195bbf --- /dev/null +++ b/Documentation~/installation.md @@ -0,0 +1,17 @@ +# Netcode for Entities Project Setup + +To setup Netcode for Entities you need to make sure you are on the correct version of the Editor. + +## Unity Editor Version + +Netcode for Entities requires you to have Unity version __2022.2.0b8__ or higher. + +## Project Setup + +1. Open the __Unity Hub__ and create a new __URP Project__. + +2. Navigate to the __Package Manager__ (Window -> Package Manager). And add the following packages using __Add package from git URL...__ under the __+__ menu at the top left of the Package Manager. + - com.unity.netcode + - com.unity.entities.graphics + +When the Package Manager is done you can continue with the [next part](networked-cube.md). \ No newline at end of file diff --git a/Documentation~/logging.md b/Documentation~/logging.md index 89ef24c..15c8bca 100644 --- a/Documentation~/logging.md +++ b/Documentation~/logging.md @@ -1,10 +1,11 @@ # Logging -NetCode comes with a builtin logging component so you can manipulate how much log information is printed. Currently it allows you to control either general NetCode log messages or ghost snapshot / packet logging separately. +Netcode for Entities comes with a builtin logging component so you can manipulate how much log information is printed. Currently it allows you to control either general logging messages or ghost snapshot / packet logging separately. -## General NetCode logs +## Generic Logging message and levels -Normal log messages will be printed to the usual log destination Unity is using (i.e. console log and Editor.log when using the Editor. You can change the log level by setting `NetCodeDebugSystem.LogLevel`. The different log levels are: +Log messages will be printed to the usual log destination Unity is using (i.e. console log and Editor.log when using the Editor).
+You can change the log level by setting `NetDebug.LogLevelType`. The different log levels are: * Debug * Notify @@ -12,11 +13,14 @@ Normal log messages will be printed to the usual log destination Unity is using * Error * Exception -The default log level is _Notify_ which has informational messages and higher importance (Notify/Warning/etc). You can set the log level to _Debug_ to get a lot of debug messages related to connection flow, ghost information and so on. This will be a lot of messages which will be most useful when debugging issues. +The default log level is _Notify_ which has informational messages and higher importance (Notify/Warning/etc). In case you want more details about connection flow, received ghosts etc you can select the _Debug_ log level. +This will emit more informative messages which will be most useful when debugging issues. ## Packet and ghost snapshot logging -You can also enable detailed log messages about ghost snapshots and how they're being written to the packets sent over the network. This will be very verbose information so should be used sparingly when debugging issues related to ghost replication. It can be enabled by adding a `EnablePacketLogging` component to the connection entity you want to debug. +You can also enable detailed log messages about ghost snapshots and how they're being written to the packets sent over the network. The `packet dump` is quite verbose and should be used sparingly when debugging issues related to ghost replication. + +The snapshot logging, can be enabled by adding a `EnablePacketLogging` component to the connection entity you want to debug. For example, to add it to every connection established you would write this in a system: @@ -30,21 +34,24 @@ protected override void OnUpdate() }).Schedule(); } ``` +Packet log dumps will go into the same directory as the normal log file on desktop platforms (Win/Mac/Lin). +On mobile (Android/iOS) platforms it will go into the persistent file location where the app will have write access. +- On Android the files are output to _/Android/data/BUNDLE_IDENTIFIER/files_ and a file manager which can see these hidden files is needed to retrieve them. +- On iOS the files are output to the app container at _/var/mobile/Containers/Data/Application/GUID/Documents_, which can be retrieved via the Xcode _Devices and Simulators_ window (select the app from the _Installed Apps_ list, click the three dots below and select _Download Container..._).
+>![NOTE]These files will not be deleted automatically and will need to be cleaned up manually, they can grow very large so it's good to be aware of this. -Each connection will get its own log file, with the world name and connection ID in the name, placed in the default log directory _Logs_, in the current working directory. - -## Default ways of enabling logging - -Logging can also be easily manipulated in the _Playmode Tools Window_ after entering playmode in the editor and by adding the `NetCodeDebugConfigAuthoring` component to a game object in a SubScene. The default methods are mostly for convenience and besides allowing changes to the log level only have a toggle to dump packet logs for all connections or none. To debug specific connections code needs to be written for it depending on the use case. - -## Packet log debug defines - -By default the packet logging works in the editor and in development builds. The added logging code can affect performance even when logging is turned off and it's therefore disabled by default in release builds. It can be forced off by adding `NETCODE_NDEBUG` define to the project settings, in the _Scripting Define Symbols_ field, in the editor. To force it off in a player build it needs to be added with the _Player Scripting Defines_ component in the `BuildConfiguration` settings. It can be forced on via the `NETCODE_DEBUG` define. +### Packet log debug defines +By default, the packet logging works in the editor and in development builds. +The added logging code can affect performance, even when logging is turned off, and it's therefore disabled by default in release builds. +It can be forced off by adding `NETCODE_NDEBUG` define to the project settings, in the _Scripting Define Symbols_ field, in the editor. -## Note +To force it off in a player build, the `NETCODE_NDEBUG` needs to be added with the _Additional Scripting Defines_ in the _DOTS_ project settings. -It can happen that the `NetDebugConfigSystem` or the system which sets the log level yet had the chance to run before the first log message appears. In which case no logging will be written regardless of the configured log level. +## Simple ways of enabling packet logging and change log levels +You can easily customise the logging level and enable packet dump by either: +- Using the _Playmode Tools Window_ after entering playmode in the editor. +- By adding the `NetCodeDebugConfigAuthoring` component to a game object in a SubScene. -See the [System Update Order](https://docs.unity3d.com/Packages/com.unity.entities@0.1/manual/system_update_order.html) ECS documentation page for more information about system ordering. +These "default methods" are mostly for convenience and besides allowing changes to the log level they provide just a toggle to dump packet logs for all connections (or none).
+To debug specific connections code needs to be written for it depending on the use case. -Packet log dumps will go into the same directory as the normal log file on desktop platforms (Win/Mac/Lin). On mobile (Android/iOS) platforms it will go into the persistent file location where the app will have write access. On Android it will be in _/Android/data/BUNDLE_IDENTIFIER/files_ and a file manager which can see these hidden files is needed to retrieve them. On iOS this will be in the app container at _/var/mobile/Containers/Data/Application/GUID/Documents_, which can be retrieved via the Xcode _Devices and Simulators_ window (select the app from the _Installed Apps_ list, click the three dots below and select _Download Container..._). These files will not be deleted automatically and will need to be cleaned up manually, they can grow very large so it's good to be aware of this. \ No newline at end of file diff --git a/Documentation~/metrics.md b/Documentation~/metrics.md index 77ed29a..6539a01 100644 --- a/Documentation~/metrics.md +++ b/Documentation~/metrics.md @@ -2,9 +2,10 @@ There are 2 ways of gathering metrics about the netcode simulation. The simplest and most straight forward way is to use the NetDbg from the Multiplayer Menu in the Editor. This will provide you with a simple web interface to view the metrics. -The second way is to create a Singleton of type `MetricsMonitorComponent` and Populate it with the data points you want to monitor. +The second way is to create a Singleton of type [MetricsMonitorComponent](https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/Unity.NetCode.MetricsMonitorComponent.html) +and populate it with the data points you want to monitor. -E.g. In the following example we create a singleton containing all data metrics available. +In the following example we create a singleton containing all data metrics available. ``` var typeList = new NativeArray(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. + +Playmode Tool + +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(); diff --git a/Runtime/Connection/WarnAboutStaleRpcSystem.cs b/Runtime/Connection/WarnAboutStaleRpcSystem.cs index a743670..0d4a06b 100644 --- a/Runtime/Connection/WarnAboutStaleRpcSystem.cs +++ b/Runtime/Connection/WarnAboutStaleRpcSystem.cs @@ -15,6 +15,7 @@ namespace Unity.NetCode [BurstCompile] public partial struct WarnAboutStaleRpcSystem : ISystem { + [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); @@ -24,6 +25,7 @@ public void OnDestroy(ref SystemState state) { } + [BurstCompile] partial struct WarnAboutStaleRpc : IJobEntity { public NetDebug netDebug; diff --git a/Runtime/Debug/NetCodeDebugConfig.cs b/Runtime/Debug/NetCodeDebugConfig.cs index 8ceb959..2df391b 100644 --- a/Runtime/Debug/NetCodeDebugConfig.cs +++ b/Runtime/Debug/NetCodeDebugConfig.cs @@ -42,7 +42,7 @@ protected override void OnUpdate() if (World.IsThinClient()) return; - var debugConfig = GetSingleton(); + var debugConfig = SystemAPI.GetSingleton(); var targetLogLevel = debugConfig.LogLevel; #if UNITY_EDITOR @@ -50,7 +50,7 @@ protected override void OnUpdate() targetLogLevel = MultiplayerPlayModePreferences.TargetLogLevel; #endif - GetSingletonRW().ValueRW.LogLevel = targetLogLevel; + SystemAPI.GetSingletonRW().ValueRW.LogLevel = targetLogLevel; var cmdBuffer = m_CmdBuffer.CreateCommandBuffer(); if (debugConfig.DumpPackets) diff --git a/Runtime/Debug/NetDebug.cs b/Runtime/Debug/NetDebug.cs index d7b7870..91b296e 100644 --- a/Runtime/Debug/NetDebug.cs +++ b/Runtime/Debug/NetDebug.cs @@ -2,6 +2,10 @@ #define NETCODE_DEBUG #endif +#if USING_OBSOLETE_METHODS_VIA_INTERNALSVISIBLETO +#pragma warning disable 0436 +#endif + using System; using System.Diagnostics; using System.IO; @@ -549,3 +553,7 @@ public static FixedString32Bytes PrintHex(ulong value) } } } + +#if USING_OBSOLETE_METHODS_VIA_INTERNALSVISIBLETO +#pragma warning restore 0436 +#endif diff --git a/Runtime/Hybrid/GhostAnimationController.cs b/Runtime/Hybrid/GhostAnimationController.cs index 760e613..b46ed8f 100644 --- a/Runtime/Hybrid/GhostAnimationController.cs +++ b/Runtime/Hybrid/GhostAnimationController.cs @@ -27,6 +27,7 @@ public struct EnableAnimationControllerPredictionUpdate : IComponentData ///
[RequireComponent(typeof(Animator), typeof(GhostPresentationGameObjectEntityOwner))] [DisallowMultipleComponent] + [HelpURL(HelpURLs.GhostAnimationController)] public class GhostAnimationController : MonoBehaviour, IRegisterPlayableData { interface IAnimationDataReference : IDisposable @@ -195,14 +196,23 @@ internal void EvaluateGraph(float deltaTime) return; if (m_ApplyRootMotion) { +#if !ENABLE_TRANSFORM_V1 + m_Transform.localPosition = m_EntityOwner.World.EntityManager.GetComponentData(m_EntityOwner.Entity).Position; + m_Transform.localRotation = m_EntityOwner.World.EntityManager.GetComponentData(m_EntityOwner.Entity).Rotation; +#else m_Transform.localPosition = m_EntityOwner.World.EntityManager.GetComponentData(m_EntityOwner.Entity).Value; m_Transform.localRotation = m_EntityOwner.World.EntityManager.GetComponentData(m_EntityOwner.Entity).Value; +#endif } m_PlayableGraph.Evaluate(deltaTime); if (m_ApplyRootMotion) { +#if !ENABLE_TRANSFORM_V1 + m_EntityOwner.World.EntityManager.SetComponentData(m_EntityOwner.Entity, LocalTransform.FromPositionRotation(m_Transform.localPosition, m_Transform.localRotation)); +#else m_EntityOwner.World.EntityManager.SetComponentData(m_EntityOwner.Entity, new Translation{Value = m_Transform.localPosition}); m_EntityOwner.World.EntityManager.SetComponentData(m_EntityOwner.Entity, new Rotation{Value = m_Transform.localRotation}); +#endif } } @@ -267,7 +277,7 @@ protected override void OnCreate() } protected override void OnUpdate() { - var predictionTick = GetSingleton().ServerTick; + var predictionTick = SystemAPI.GetSingleton().ServerTick; var prevTick = predictionTick; prevTick.Decrement(); var deltaTime = SystemAPI.Time.DeltaTime; diff --git a/Runtime/Hybrid/GhostPresentationGameObject.cs b/Runtime/Hybrid/GhostPresentationGameObject.cs index 77514e6..30741a6 100644 --- a/Runtime/Hybrid/GhostPresentationGameObject.cs +++ b/Runtime/Hybrid/GhostPresentationGameObject.cs @@ -1,6 +1,7 @@ using Unity.Entities; using UnityEngine; using System.Collections.Generic; +using Unity.Burst; using Unity.Transforms; using Unity.Collections; using UnityEngine.Jobs; @@ -164,9 +165,19 @@ protected override void OnCreate() m_GhostPresentationGameObjectSystem = World.GetExistingSystemManaged(); RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly())); } + [BurstCompile] struct TransformUpdateJob : IJobParallelForTransform { [ReadOnly] public NativeList Entities; +#if !ENABLE_TRANSFORM_V1 + [ReadOnly] public ComponentLookup TransformFromEntity; + public void Execute(int index, TransformAccess transform) + { + var ent = Entities[index]; + transform.localPosition = TransformFromEntity[ent].Position; + transform.localRotation = TransformFromEntity[ent].Rotation; + } +#else [ReadOnly] public ComponentLookup TranslationFromEntity; [ReadOnly] public ComponentLookup RotationFromEntity; public void Execute(int index, TransformAccess transform) @@ -175,14 +186,19 @@ public void Execute(int index, TransformAccess transform) transform.localPosition = TranslationFromEntity[ent].Value; transform.localRotation = RotationFromEntity[ent].Value; } +#endif } protected override void OnUpdate() { var transformJob = new TransformUpdateJob { Entities = m_GhostPresentationGameObjectSystem.m_Entities, +#if !ENABLE_TRANSFORM_V1 + TransformFromEntity = GetComponentLookup(true), +#else TranslationFromEntity = GetComponentLookup(true), RotationFromEntity = GetComponentLookup(true) +#endif }; Dependency = transformJob.Schedule(m_GhostPresentationGameObjectSystem.m_Transforms, Dependency); } diff --git a/Runtime/Hybrid/GhostPresentationGameObjectEntityOwner.cs b/Runtime/Hybrid/GhostPresentationGameObjectEntityOwner.cs index 1fcf717..0053352 100644 --- a/Runtime/Hybrid/GhostPresentationGameObjectEntityOwner.cs +++ b/Runtime/Hybrid/GhostPresentationGameObjectEntityOwner.cs @@ -9,6 +9,7 @@ namespace Unity.NetCode.Hybrid /// instance. /// [DisallowMultipleComponent] + [HelpURL(HelpURLs.GhostPresentationGameObjectEntityOwner)] public class GhostPresentationGameObjectEntityOwner : MonoBehaviour { /// diff --git a/Runtime/Hybrid/HelpURLs.cs b/Runtime/Hybrid/HelpURLs.cs new file mode 100644 index 0000000..cddac2d --- /dev/null +++ b/Runtime/Hybrid/HelpURLs.cs @@ -0,0 +1,9 @@ +namespace Unity.NetCode.Hybrid +{ + internal static class HelpURLs + { + const string k_BaseUrl = "https://docs.unity3d.com/Packages/com.unity.netcode@latest/index.html?subfolder=/api/"; + internal const string GhostAnimationController = k_BaseUrl + "Unity.NetCode.Hybrid.GhostAnimationController.html"; + internal const string GhostPresentationGameObjectEntityOwner = k_BaseUrl + "Unity.NetCode.Hybrid.GhostPresentationGameObjectEntityOwner.html"; + } +} diff --git a/Runtime/Hybrid/HelpURLs.cs.meta b/Runtime/Hybrid/HelpURLs.cs.meta new file mode 100644 index 0000000..51e89c4 --- /dev/null +++ b/Runtime/Hybrid/HelpURLs.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 26fa8e7f1e7746709b3fe092ef836f1a +timeCreated: 1666430766 \ No newline at end of file diff --git a/Runtime/Hybrid/Unity.NetCode.Hybrid.asmdef b/Runtime/Hybrid/Unity.NetCode.Hybrid.asmdef index d107ecf..ffdd97c 100644 --- a/Runtime/Hybrid/Unity.NetCode.Hybrid.asmdef +++ b/Runtime/Hybrid/Unity.NetCode.Hybrid.asmdef @@ -19,6 +19,27 @@ "!UNITY_DOTSRUNTIME", "!UNITY_DISABLE_MANAGED_COMPONENTS" ], - "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" + } + ], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Runtime/Physics/Hybrid/NetCodePhysicsConfig.cs b/Runtime/Physics/Hybrid/NetCodePhysicsConfig.cs index cc6d5ff..53c4840 100644 --- a/Runtime/Physics/Hybrid/NetCodePhysicsConfig.cs +++ b/Runtime/Physics/Hybrid/NetCodePhysicsConfig.cs @@ -12,6 +12,7 @@ namespace Unity.NetCode /// the , components are automatically added to it based on these settings. /// [DisallowMultipleComponent] + [HelpURL(Authoring.HelpURLs.NetCodePhysicsConfig)] public sealed class NetCodePhysicsConfig : MonoBehaviour { /// diff --git a/Runtime/Physics/Hybrid/NetCodePhysicsInspector.cs b/Runtime/Physics/Hybrid/NetCodePhysicsInspector.cs index b626ecf..563f3f3 100644 --- a/Runtime/Physics/Hybrid/NetCodePhysicsInspector.cs +++ b/Runtime/Physics/Hybrid/NetCodePhysicsInspector.cs @@ -23,6 +23,8 @@ private void OnEnable() public override void OnInspectorGUI() { serializedObject.Update(); + using (new EditorGUI.DisabledScope(true)) + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Script"), true); EditorGUILayout.PropertyField(EnableLagCompensation, new GUIContent("Lag Compensation")); if (EnableLagCompensation.boolValue) { diff --git a/Runtime/Physics/PhysicsVelocityVariant.cs b/Runtime/Physics/PhysicsVelocityVariant.cs index df5e288..664f3bb 100644 --- a/Runtime/Physics/PhysicsVelocityVariant.cs +++ b/Runtime/Physics/PhysicsVelocityVariant.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using Unity.Physics.GraphicsIntegration; @@ -31,4 +33,33 @@ public struct PhysicsVelocityDefaultVariant public struct PhysicsGraphicalSmoothingDefaultVariant { } + + /// + /// Optionally register the default variant to use for the and the + /// . + /// + /// It will never override the default assignment for the `PhysicsVelocity` nor the `PhysicsGraphicalSmoothing` components + /// if they are already present in the map. + /// Any system deriving from will take precendence, even if they are created + /// after this system. + /// + /// + [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation | + WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.BakingSystem)] + [CreateAfter(typeof(GhostComponentSerializerCollectionSystemGroup))] + [UpdateInGroup(typeof(DefaultVariantSystemGroup), OrderLast = true)] + public sealed partial class PhysicsDefaultVariantSystem : SystemBase + { + protected override void OnCreate() + { + var rules = World.GetExistingSystemManaged().DefaultVariantRules; + rules.TrySetDefaultVariant(ComponentType.ReadWrite(), DefaultVariantSystemBase.Rule.OnlyParents(typeof(PhysicsVelocityDefaultVariant)), this); + rules.TrySetDefaultVariant(ComponentType.ReadWrite(), DefaultVariantSystemBase.Rule.OnlyParents(typeof(PhysicsGraphicalSmoothingDefaultVariant)), this); + Enabled = false; + } + + protected override void OnUpdate() + { + } + } } diff --git a/Runtime/Rpc/RpcCommandRequest.cs b/Runtime/Rpc/RpcCommandRequest.cs index 485101e..1bf049e 100644 --- a/Runtime/Rpc/RpcCommandRequest.cs +++ b/Runtime/Rpc/RpcCommandRequest.cs @@ -166,7 +166,7 @@ void LambdaMethod(Entity entity, int orderIndex, in SendRpcCommandRequestCompone public void Execute(ArchetypeChunk chunk, int orderIndex) { var entities = chunk.GetNativeArray(entitiesType); - var rpcRequests = chunk.GetNativeArray(rpcRequestType); + var rpcRequests = chunk.GetNativeArray(ref rpcRequestType); if (ComponentType.ReadOnly().IsZeroSized) { TActionRequest action = default; @@ -177,7 +177,7 @@ public void Execute(ArchetypeChunk chunk, int orderIndex) } else { - var actions = chunk.GetNativeArray(actionRequestType); + var actions = chunk.GetNativeArray(ref actionRequestType); for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i) { LambdaMethod(entities[i], orderIndex, rpcRequests[i], actions[i]); diff --git a/Runtime/Rpc/RpcSystem.cs b/Runtime/Rpc/RpcSystem.cs index faf5d8f..a63916d 100644 --- a/Runtime/Rpc/RpcSystem.cs +++ b/Runtime/Rpc/RpcSystem.cs @@ -241,10 +241,10 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo Assert.IsFalse(useEnabledMask); var entities = chunk.GetNativeArray(entityType); - var rpcInBuffer = chunk.GetBufferAccessor(inBufferType); - var rpcOutBuffer = chunk.GetBufferAccessor(outBufferType); - var connections = chunk.GetNativeArray(connectionType); - var acks = chunk.GetNativeArray(ackType); + var rpcInBuffer = chunk.GetBufferAccessor(ref inBufferType); + var rpcOutBuffer = chunk.GetBufferAccessor(ref outBufferType); + var connections = chunk.GetNativeArray(ref connectionType); + var acks = chunk.GetNativeArray(ref ackType); var deserializeState = new RpcDeserializerState {ghostMap = ghostMap}; for (int i = 0; i < rpcInBuffer.Length; ++i) { diff --git a/Runtime/Simulator/MultiplayerPlayModePreferences.cs b/Runtime/Simulator/MultiplayerPlayModePreferences.cs index 2d640e8..23a7649 100644 --- a/Runtime/Simulator/MultiplayerPlayModePreferences.cs +++ b/Runtime/Simulator/MultiplayerPlayModePreferences.cs @@ -63,17 +63,11 @@ public static ClientServerBootstrap.PlayType RequestedPlayType set => EditorPrefs.SetInt(s_PlayModeTypeKey, (int) value); } - public enum ServerWorldDataToLoad + private static string s_SimulateDedicatedServer = s_PrefsKeyPrefix + "SimulateDedicatedServer"; + public static bool SimulateDedicatedServer { - Server, - ClientAndServer - } - - private static string s_ServerWorldDataType = s_PrefsKeyPrefix + "ServerWorldData_Type"; - public static ServerWorldDataToLoad ServerLoadDataType - { - get => (ServerWorldDataToLoad) EditorPrefs.GetInt(s_ServerWorldDataType, (int) ServerWorldDataToLoad.ClientAndServer); - set => EditorPrefs.SetInt(s_ServerWorldDataType, (int) value); + get => EditorPrefs.GetBool(s_SimulateDedicatedServer, false); + set => EditorPrefs.SetBool(s_SimulateDedicatedServer, value); } /// diff --git a/Runtime/Simulator/SimulatorPreset.cs b/Runtime/Simulator/SimulatorPreset.cs index 403b0a6..94a49fa 100644 --- a/Runtime/Simulator/SimulatorPreset.cs +++ b/Runtime/Simulator/SimulatorPreset.cs @@ -6,14 +6,14 @@ namespace Unity.NetCode /// /// Presets for the com.unity.transport simulator. /// Allows developers to simulate a variety of network conditions. - /// - /// + /// + /// /// [Serializable] public struct SimulatorPreset { /// Users can modify simulator preset values directly. This preset is called "custom". - internal const string k_CustomProfileKey = "Custom"; + internal const string k_CustomProfileKey = "Custom / User Defined"; const string k_CustomProfileTooltip = "Custom indicates that you have modified individual simulator values yourself."; const string k_PoorMobileTooltip = "Extremely poor connection quality, completely unsuitable for synchronous multiplayer gaming due to exceptionally high latency. Turn based games may work."; const string k_DecentMobileTooltip = "Suitable for synchronous multiplayer, but expect connection instability.\n\nExpect to handle players dropping frequently, and dropping their connection entirely. I.e. Ensure you handle reconnections and quickly detect (and display) wifi issues."; @@ -31,9 +31,23 @@ public struct SimulatorPreset const string k_InternationalAverage = "Represents an \"average\" connection from a player connecting to a server hosted outside their region." + k_InternationalDisclaimer + "I.e. Half of all " + k_PlayersAsGoodOrBetter; const string k_InternationalPoor = "Represents a \"poor\" connection from a player connecting to a server hosted outside their region." + k_InternationalDisclaimer + "I.e. 95% of " + k_PlayersAsGoodOrBetter; + /// + /// The most common profiles, including custom debug ones. + /// Last updated Q3 2022. + /// + /// To append to. + public static void AppendBaseSimulatorPresets(List list) + { + list.Add(new SimulatorPreset(k_CustomProfileKey, -1, -1, -1, k_CustomProfileTooltip)); + list.Add(new SimulatorPreset("Custom / No Internet", 1000, 1000, 100, "Simulate the server becoming completely unreachable.")); + list.Add(new SimulatorPreset("Custom / Unplayable Internet", 300, 400, 30, "Simulate barely having a connection at all, to observe what your users will experience when the internet is good enough to connect (sometimes), but not good enough to play.\n\nIt may take multiple attempts for the driver to connect.\n\nWe recommend detecting a \"minimum threshold of playable\", and to exclude (and inform) users when below this threshold.")); + + BuildProfiles(list, true, "Broadband [WIFI] / ", 1, 1, 1, k_MobileWifiDisclaimer); + } + /// /// These are best-estimate approximations for mobile connection types, informed by real world data. - /// Last updated Q2 2022. + /// Last updated Q3 2022. /// Sources: /// - Developers [Multiplayer, Support and Customers] /// - https://unity.com/products/multiplay @@ -41,10 +55,8 @@ public struct SimulatorPreset /// - https://www.4g.co.uk/how-fast-is-4g/ /// /// To append to. - public static void AppendDefaultMobileSimulatorProfiles(List list) + public static void AppendAdditionalMobileSimulatorProfiles(List list) { - list.Add(new SimulatorPreset(k_CustomProfileKey, -1, -1, -1, k_CustomProfileTooltip)); - BuildProfiles(list, true, "Broadband [WIFI] / ", 1, 1, 1, k_MobileWifiDisclaimer); BuildProfiles(list, false, "2G [!] [CDMA & GSM, '00] / ", 200, 20, 5, k_PoorMobileTooltip); BuildProfiles(list, false, "2.5G [!] [GPRS, G, '00] / ", 180, 15, 5, k_PoorMobileTooltip); BuildProfiles(list, false, "2.75G [!] [Edge, E, '06] / ", 160, 15, 5, k_PoorMobileTooltip); @@ -58,16 +70,15 @@ public static void AppendDefaultMobileSimulatorProfiles(List li /// /// These are best-estimate approximations of PC and Console connection types, informed by real world data. - /// Last updated Q2 2022. + /// Last updated Q3 2022. /// Sources: /// - Developers [Multiplayer, Support and Customers] /// - https://unity.com/products/multiplay /// /// To append to. - public static void AppendDefaultPCConsoleSimulatorProfiles(List list) + public static void AppendAdditionalPCSimulatorPresets(List list) { - list.Add(new SimulatorPreset("LAN - Local Area Network", 1, 1, 0, "Playing on LAN is generally <1ms (i.e. simulator off), but we've included it for convenience.")); - BuildProfiles(list, true, "Home Broadband - ", 0, 0, 1, null); + list.Add(new SimulatorPreset("LAN [Local Area Network]", 1, 1, 1, "Playing on LAN is generally <1ms (i.e. simulator off), but we've included it for convenience.")); } /// Builds sub-profiles for your profile. E.g. 4 regional options for your custom profile. @@ -108,17 +119,20 @@ public static void DefaultInUseSimulatorPresets(out string presetGroupName, List if (MultiplayerPlayModePreferences.ShowAllSimulatorPresets) { presetGroupName = "All Presets"; - AppendDefaultMobileSimulatorProfiles(appendPresets); - AppendDefaultPCConsoleSimulatorProfiles(appendPresets); + AppendBaseSimulatorPresets(appendPresets); + AppendAdditionalPCSimulatorPresets(appendPresets); + AppendAdditionalMobileSimulatorProfiles(appendPresets); } else { #if UNITY_IOS || UNITY_ANDROID presetGroupName = "Mobile Presets"; - AppendDefaultMobileSimulatorProfiles(appendPresets); + AppendBaseSimulatorPresets(appendPresets); + AppendAdditionalMobileSimulatorProfiles(appendPresets); #else presetGroupName = "PC & Console Presets"; - AppendDefaultPCConsoleSimulatorProfiles(appendPresets); + AppendBaseSimulatorPresets(appendPresets); + AppendAdditionalPCSimulatorPresets(appendPresets); #endif } } diff --git a/Runtime/Snapshot/DefaultTranslationSmoothingAction.cs b/Runtime/Snapshot/DefaultTranslationSmoothingAction.cs index af5ed7c..78f86cd 100644 --- a/Runtime/Snapshot/DefaultTranslationSmoothingAction.cs +++ b/Runtime/Snapshot/DefaultTranslationSmoothingAction.cs @@ -75,8 +75,13 @@ class DeltaKey {} [MonoPInvokeCallback(typeof(GhostPredictionSmoothing.SmoothingActionDelegate))] private static void SmoothingAction(IntPtr currentData, IntPtr previousData, IntPtr usrData) { +#if !ENABLE_TRANSFORM_V1 + ref var trans = ref UnsafeUtility.AsRef((void*)currentData); + ref var backup = ref UnsafeUtility.AsRef((void*)previousData); +#else ref var trans = ref UnsafeUtility.AsRef((void*)currentData); ref var backup = ref UnsafeUtility.AsRef((void*)previousData); +#endif float maxDist = DefaultStaticUserParams.maxDist.Data; float delta = DefaultStaticUserParams.delta.Data; @@ -88,11 +93,19 @@ private static void SmoothingAction(IntPtr currentData, IntPtr previousData, Int delta = userParam.delta; } +#if !ENABLE_TRANSFORM_V1 + var dist = math.distance(trans.Position, backup.Position); + if (dist < maxDist && dist > delta && dist > 0) + { + trans.Position = backup.Position + (trans.Position - backup.Position) * delta / dist; + } +#else var dist = math.distance(trans.Value, backup.Value); if (dist < maxDist && dist > delta && dist > 0) { trans.Value = backup.Value + (trans.Value - backup.Value) * delta / dist; } +#endif } } } diff --git a/Runtime/Snapshot/GhostChunkSerializer.cs b/Runtime/Snapshot/GhostChunkSerializer.cs index 4955740..b275dfb 100644 --- a/Runtime/Snapshot/GhostChunkSerializer.cs +++ b/Runtime/Snapshot/GhostChunkSerializer.cs @@ -12,6 +12,13 @@ namespace Unity.NetCode { + internal enum SerializeEnitiesResult + { + Unknown = 0, + Ok, + Failed, + Abort, + } internal unsafe struct GhostChunkSerializer { public DynamicBuffer GhostComponentCollection; @@ -387,6 +394,25 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng throw new InvalidOperationException("Prespawn baseline does not have have space for dynamic buffer data"); } + /// + /// - Writes predictive component data into the snapshot. + /// - Writes the snapshot into the dataStream writer. + /// + /// Recursive when iterating over ghost groups. + /// Transport write stream to write "prediction-compressed" snapshots into. + /// + /// + /// Chunk containing these ghosts (and thus their components). + /// Index of the first entity to process. + /// Index of the NEXT entity (PASSED the LAST entity) to process. + /// + /// + /// + /// + /// + /// Stores 2 ints per component, per entity. [1st] Writer bit offset to the start of this components writes. [2nd] Num bits written for this component. + /// + /// private int SerializeEntities(ref DataStreamWriter dataStream, out int skippedEntityCount, out uint anyChangeMask, int ghostType, ArchetypeChunk chunk, int startIndex, int endIndex, bool useSingleBaseline, in CurrentSnapshotState currentSnapshot, byte** baselinesPerEntity = null, int* sameBaselinePerEntity = null, int* dynamicDataLenPerEntity = null, int* entityStartBit = null) @@ -418,10 +444,10 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng int enableableMaskUints = GhostComponentSerializer.ChangeMaskArraySizeInUInts(typeData.EnableableBits); var ghostEntities = chunk.GetNativeArray(entityType); - var ghosts = chunk.GetNativeArray(ghostComponentType); + var ghosts = chunk.GetNativeArray(ref ghostComponentType); NativeArray ghostSystemState = default; if (currentSnapshot.SnapshotData != null) - ghostSystemState = chunk.GetNativeArray(ghostSystemStateType); + ghostSystemState = chunk.GetNativeArray(ref ghostSystemStateType); byte* snapshot; if (currentSnapshot.SnapshotData == null) @@ -442,6 +468,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng sameBaselinePerEntity = tempSameBaselinePerEntity; if (entityStartBit == null) entityStartBit = tempEntityStartBit; + int baseline0 = numAvailableBaselines; int baseline1 = numAvailableBaselines; int baseline2 = numAvailableBaselines; @@ -502,10 +529,10 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng baselinesPerEntity[offset+2] = (currentSnapshot.AvailableBaselines[baseline2].snapshot) + ent*snapshotSize; } - if (baseline0 == numAvailableBaselines && chunk.Has(prespawnBaselineTypeHandle) && chunk.Has(PrespawnIndexType) && + if (baseline0 == numAvailableBaselines && chunk.Has(ref prespawnBaselineTypeHandle) && chunk.Has(ref PrespawnIndexType) && (ghostStateData.GetGhostState(ghostSystemState[ent]).Flags & ConnectionStateData.GhostStateFlags.CantUsePrespawnBaseline) == 0) { - var prespawnBaselines = chunk.GetBufferAccessor(prespawnBaselineTypeHandle); + var prespawnBaselines = chunk.GetBufferAccessor(ref prespawnBaselineTypeHandle); ValidatePrespawnBaseline(ghostEntities[ent], ghosts[ent].ghostId,ent,prespawnBaselines.Length); if (prespawnBaselines[ent].Length > 0) { @@ -522,9 +549,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng // Update the end index to skip irrelevant entities at the end of the chunk int realEndIndex = endIndex; endIndex = lastRelevantEntity; - - int bitCountsPerComponent = endIndex-startIndex; - + int entityOffset = endIndex-startIndex; int snapshotOffset = GhostComponentSerializer.SnapshotSizeAligned(sizeof(uint) + (changeMaskUints * sizeof(uint)) + (enableableMaskUints * sizeof(uint))); @@ -544,7 +569,9 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng var oldTempWriter = tempWriter; - if (chunk.Has(preSerializedGhostType) && SnapshotPreSerializeData.TryGetValue(chunk, out var preSerializedSnapshot)) + SnapshotPreSerializeData preSerializedSnapshot = default; + var hasPreserializeData = chunk.Has(preSerializedGhostType) && SnapshotPreSerializeData.TryGetValue(chunk, out preSerializedSnapshot); + if (hasPreserializeData) { UnsafeUtility.MemCpy(snapshot, (byte*)preSerializedSnapshot.Data+snapshotSize*startIndex, snapshotSize*(endIndex-startIndex)); // If this chunk has been processed for this tick before we cannot copy the dynamic snapshot data since doing so would @@ -562,7 +589,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng if (GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { ComponentScopeBegin(serializerIdx); - GhostComponentCollection[serializerIdx].PostSerializeBuffer.Ptr.Invoke((IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*bitCountsPerComponent*comp), (IntPtr)snapshotDynamicDataPtr, (IntPtr)dynamicDataLenPerEntity, dynamicSnapshotDataCapacity + dynamicDataHeaderSize); + GhostComponentCollection[serializerIdx].PostSerializeBuffer.Ptr.Invoke((IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*entityOffset*comp), (IntPtr)snapshotDynamicDataPtr, (IntPtr)dynamicDataLenPerEntity, dynamicSnapshotDataCapacity + dynamicDataHeaderSize); ComponentScopeEnd(serializerIdx); snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostSystemConstants.DynamicBufferComponentSnapshotSize); @@ -570,8 +597,10 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng } else { + // TODO: Ensure these pointer invocations are NOT called in the ZeroSize case (but we must update entityStartBit)! + // Which means we can remove the #ifdef in Serializer Template. ComponentScopeBegin(serializerIdx); - GhostComponentCollection[serializerIdx].PostSerialize.Ptr.Invoke((IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*bitCountsPerComponent*comp)); + GhostComponentCollection[serializerIdx].PostSerialize.Ptr.Invoke((IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*entityOffset*comp)); ComponentScopeEnd(serializerIdx); snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostComponentCollection[serializerIdx].SnapshotSize); snapshotMaskOffsetInBits += GhostComponentCollection[serializerIdx].ChangeMaskBits; @@ -594,7 +623,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng //It might still work in some cases but if this snapshot is then part of the history and used for //interpolated data we might get incorrect results - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var handle = ghostChunkComponentTypesPtr[compIdx]; enableableMaskOffset = UpdateEnableableMasks(chunk, startIndex, endIndex, ref handle, snapshot, changeMaskUints, enableableMaskOffset, snapshotSize); @@ -602,9 +631,10 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng if (GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { + // Buffers cannot be zero sized, so no need to guard here. byte** compData = tempComponentDataPerEntity; int* compDataLen = tempComponentDataLenPerEntity; - if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { var bufData = chunk.GetUntypedBufferAccessor(ref ghostChunkComponentTypesPtr[compIdx]); for (int ent = startIndex; ent < endIndex; ++ent) @@ -622,7 +652,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng } } ComponentScopeBegin(serializerIdx); - GhostComponentCollection[serializerIdx].SerializeBuffer.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr)compData, (IntPtr)compDataLen, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*bitCountsPerComponent*comp), (IntPtr)snapshotDynamicDataPtr, ref snapshotDynamicDataOffset, (IntPtr)dynamicDataLenPerEntity, dynamicSnapshotDataCapacity + dynamicDataHeaderSize); + GhostComponentCollection[serializerIdx].SerializeBuffer.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr)compData, (IntPtr)compDataLen, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*entityOffset*comp), (IntPtr)snapshotDynamicDataPtr, ref snapshotDynamicDataOffset, (IntPtr)dynamicDataLenPerEntity, dynamicSnapshotDataCapacity + dynamicDataHeaderSize); ComponentScopeEnd(serializerIdx); snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostSystemConstants.DynamicBufferComponentSnapshotSize); snapshotMaskOffsetInBits += GhostSystemConstants.DynamicBufferComponentMaskBits; @@ -630,13 +660,14 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng else { byte* compData = null; - if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (GhostComponentCollection[serializerIdx].HasGhostFields && chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { - compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); compData += startIndex * compSize; } + ComponentScopeBegin(serializerIdx); - GhostComponentCollection[serializerIdx].Serialize.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr)compData, compSize, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*bitCountsPerComponent*comp)); + GhostComponentCollection[serializerIdx].Serialize.Ptr.Invoke((IntPtr) UnsafeUtility.AddressOf(ref serializerState), (IntPtr) snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr) compData, compSize, endIndex - startIndex, (IntPtr) baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr) (entityStartBit + 2 * entityOffset * comp)); ComponentScopeEnd(serializerIdx); snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostComponentCollection[serializerIdx].SnapshotSize); snapshotMaskOffsetInBits += GhostComponentCollection[serializerIdx].ChangeMaskBits; @@ -644,7 +675,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng } if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[typeData.FirstComponent + comp].ComponentIndex; @@ -661,9 +692,9 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng { var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[typeData.FirstComponent + comp].EntityIndex].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var entityIndex = childChunk.IndexInChunk; var handle = ghostChunkComponentTypesPtr[compIdx]; @@ -683,7 +714,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng } enableableMaskOffset++; ComponentScopeBegin(serializerIdx); - GhostComponentCollection[serializerIdx].SerializeBuffer.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr)compData, (IntPtr)compDataLen, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*bitCountsPerComponent*comp), (IntPtr)snapshotDynamicDataPtr, ref snapshotDynamicDataOffset, (IntPtr)dynamicDataLenPerEntity, dynamicSnapshotDataCapacity + dynamicDataHeaderSize); + GhostComponentCollection[serializerIdx].SerializeBuffer.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr)compData, (IntPtr)compDataLen, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*entityOffset*comp), (IntPtr)snapshotDynamicDataPtr, ref snapshotDynamicDataOffset, (IntPtr)dynamicDataLenPerEntity, dynamicSnapshotDataCapacity + dynamicDataHeaderSize); ComponentScopeEnd(serializerIdx); snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostSystemConstants.DynamicBufferComponentSnapshotSize); snapshotMaskOffsetInBits += GhostSystemConstants.DynamicBufferComponentMaskBits; @@ -697,25 +728,29 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[typeData.FirstComponent + comp].EntityIndex].Value; compData[ent-startIndex] = null; - //We can skip here, becase the memory buffer offset is computed using the start-end entity indices - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + //We can skip here, because the memory buffer offset is computed using the start-end entity indices + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var entityIndex = childChunk.IndexInChunk; var handle = ghostChunkComponentTypesPtr[compIdx]; - UpdateEnableableMasks(childChunk.Chunk, entityIndex, entityIndex+1, ref handle, snapshotPtr, changeMaskUints, enableableMaskOffset, snapshotSize); + UpdateEnableableMasks(childChunk.Chunk, entityIndex, entityIndex + 1, ref handle, snapshotPtr, changeMaskUints, enableableMaskOffset, snapshotSize); } - compData[ent-startIndex] = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); - compData[ent-startIndex] += childChunk.IndexInChunk * compSize; + if (GhostComponentCollection[serializerIdx].HasGhostFields) + { + compData[ent - startIndex] = (byte*) childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + compData[ent - startIndex] += childChunk.IndexInChunk * compSize; + } } + snapshotPtr += snapshotSize; } enableableMaskOffset++; ComponentScopeBegin(serializerIdx); - GhostComponentCollection[serializerIdx].SerializeChild.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr)compData, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*bitCountsPerComponent*comp)); + GhostComponentCollection[serializerIdx].SerializeChild.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshot, snapshotOffset, snapshotSize, snapshotMaskOffsetInBits, (IntPtr)compData, endIndex - startIndex, (IntPtr)baselinesPerEntity, ref tempWriter, ref compressionModel, (IntPtr)(entityStartBit+2*entityOffset*comp)); ComponentScopeEnd(serializerIdx); snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostComponentCollection[serializerIdx].SnapshotSize); snapshotMaskOffsetInBits += GhostComponentCollection[serializerIdx].ChangeMaskBits; @@ -743,9 +778,9 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng } // Copy the content per entity from the temporary stream to the output stream in the correct order - var writerData = (uint*)tempWriter.AsNativeArray().GetUnsafeReadOnlyPtr(); + var writerData = (uint*)tempWriter.AsNativeArray().GetUnsafePtr(); uint zeroChangeMask = 0; - uint baseGhostId = chunk.Has(PrespawnIndexType) ? PrespawnHelper.PrespawnGhostIdBase : 0; + uint baseGhostId = chunk.Has(ref PrespawnIndexType) ? PrespawnHelper.PrespawnGhostIdBase : 0; for (int ent = startIndex; ent < endIndex; ++ent) { @@ -879,7 +914,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng } if (remainingChangeBits > 0) GhostComponentSerializer.CopyToChangeMask((IntPtr)changeMasks, 0, curMaskOffsetInBits+subMaskOffset, remainingChangeBits); - entityStartBit[(bitCountsPerComponent*comp + entOffset)*2+1] = 0; + entityStartBit[(entityOffset*comp + entOffset)*2+1] = 0; // TODO: buffers could also reduce the required dynamic buffer size to save some memory on clients } curMaskOffsetInBits += changeBits; @@ -936,7 +971,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng if (anyChangeMaskThisEntity != 0) { for (int comp = 0; comp < typeData.NumComponents; ++comp) - ghostSizeInBits += entityStartBit[(bitCountsPerComponent*comp + entOffset)*2+1]; + ghostSizeInBits += entityStartBit[(entityOffset * comp + entOffset) * 2 + 1]; } dataStream.WritePackedUIntDelta((uint)(ghostSizeInBits+headerLen), 0, compressionModel); while (headerLen > 32) @@ -975,11 +1010,11 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng if (anyChangeMaskThisEntity != 0) { - PacketDumpComponentSize(typeData, entityStartBit, bitCountsPerComponent, entOffset); + PacketDumpComponentSize(typeData, entityStartBit, entityOffset, entOffset); for (int comp = 0; comp < typeData.NumComponents; ++comp) { - int start = entityStartBit[(bitCountsPerComponent*comp + entOffset)*2]; - int len = entityStartBit[(bitCountsPerComponent*comp + entOffset)*2+1]; + int start = entityStartBit[(entityOffset*comp + entOffset)*2]; + int len = entityStartBit[(entityOffset*comp + entOffset)*2+1]; if (len > 0) { while (len > 32) @@ -1002,7 +1037,7 @@ private static void ValidatePrespawnSpaceForDynamicData(int prespawnBaselineLeng if (typeData.IsGhostGroup != 0) { ghostGroupMarker.Begin(); - var ghostGroup = chunk.GetBufferAccessor(ghostGroupType)[ent]; + var ghostGroup = chunk.GetBufferAccessor(ref ghostGroupType)[ent]; // Serialize all other ghosts in the group, this also needs to be handled correctly in the receive system dataStream.WritePackedUInt((uint)ghostGroup.Length, compressionModel); PacketDumpBeginGroup(ghostGroup.Length); @@ -1087,7 +1122,7 @@ private bool CanSerializeGroup(in DynamicBuffer ghostGroup) return false; } #if ENABLE_UNITY_COLLECTIONS_CHECKS - if (!groupChunk.Chunk.Has(ghostChildEntityComponentType)) + if (!groupChunk.Chunk.Has(ref ghostChildEntityComponentType)) throw new InvalidOperationException("Ghost group contains an member which does not have a GhostChildEntityComponent."); #endif // Entity does not have valid state initialized yet, wait some more @@ -1110,14 +1145,14 @@ private bool SerializeGroup(ref DataStreamWriter dataStream, ref StreamCompressi throw new InvalidOperationException("Ghost group member does not have state."); var childGhostType = chunkState.ghostType; #if ENABLE_UNITY_COLLECTIONS_CHECKS - var ghostComp = groupChunk.Chunk.GetNativeArray(ghostComponentType); + var ghostComp = groupChunk.Chunk.GetNativeArray(ref ghostComponentType); if (ghostComp[groupChunk.IndexInChunk].ghostType >= 0 && ghostComp[groupChunk.IndexInChunk].ghostType != childGhostType) throw new InvalidOperationException("Ghost group member has invalid ghost type."); #endif ValidateNoNestedGhostGroups(GhostTypeCollection[childGhostType].IsGhostGroup); dataStream.WritePackedUInt((uint)childGhostType, compressionModel); dataStream.WritePackedUInt(1, compressionModel); - dataStream.WriteRawBits(groupChunk.Chunk.Has(prespawnBaselineTypeHandle) ? 1u : 0, 1); + dataStream.WriteRawBits(groupChunk.Chunk.Has(ref prespawnBaselineTypeHandle) ? 1u : 0, 1); PacketDumpGroupItem(childGhostType); var groupSnapshot = default(CurrentSnapshotState); @@ -1138,7 +1173,7 @@ private bool SerializeGroup(ref DataStreamWriter dataStream, ref StreamCompressi bool clearEntityArray = true; if (snapshotIndex[baselineIndex] != currentTick.SerializedData) { - // The chunk hisotry only needs to be updated once per frame, this is the first time we are using this chunk this frame + // The chunk history only needs to be updated once per frame, this is the first time we are using this chunk this frame // TODO: Updating the chunk history is only required if there has been a structural change - should skip it as an optimization UpdateChunkHistory(childGhostType, groupChunk.Chunk, chunkState, dataSize); snapshotIndex[writeIndex] = currentTick.SerializedData; @@ -1165,7 +1200,7 @@ private bool SerializeGroup(ref DataStreamWriter dataStream, ref StreamCompressi int sameBaselinePerEntity; int dynamicDataLenPerEntity; var entityStartBit = stackalloc int[GhostTypeCollection[chunkState.ghostType].NumComponents*2]; - if (SerializeEntities(ref dataStream, out var _, out var _, childGhostType, groupChunk.Chunk, groupChunk.IndexInChunk, groupChunk.IndexInChunk+1, useSingleBaseline, groupSnapshot, + if (SerializeEntities(ref dataStream, out _, out _, childGhostType, groupChunk.Chunk, groupChunk.IndexInChunk, groupChunk.IndexInChunk+1, useSingleBaseline, groupSnapshot, baselinesPerEntity, &sameBaselinePerEntity, &dynamicDataLenPerEntity, entityStartBit) != groupChunk.IndexInChunk+1) { // FIXME: this does not work if a group member is itself the root of a group since it can fail to roll back state to compress against in that case. This is the reason nested ghost groups are not supported @@ -1192,7 +1227,7 @@ private bool SerializeGroup(ref DataStreamWriter dataStream, ref StreamCompressi //to store all the dynamic buffer contents (if any) private unsafe int GatherDynamicBufferSize(in ArchetypeChunk chunk, int startIndex, int ghostType) { - if (chunk.Has(preSerializedGhostType) && SnapshotPreSerializeData.TryGetValue(chunk, out var preSerializedSnapshot)) + if (chunk.Has(ref preSerializedGhostType) && SnapshotPreSerializeData.TryGetValue(chunk, out var preSerializedSnapshot)) { return preSerializedSnapshot.DynamicSize; } @@ -1214,9 +1249,9 @@ private unsafe int GatherDynamicBufferSize(in ArchetypeChunk chunk, int startInd int UpdateGhostRelevancy(ArchetypeChunk chunk, int startIndex, byte* relevancyData, in GhostChunkSerializationState chunkState, int snapshotSize, out bool hasSpawns) { hasSpawns = false; - var ghost = chunk.GetNativeArray(ghostComponentType); + var ghost = chunk.GetNativeArray(ref ghostComponentType); var ghostEntities = chunk.GetNativeArray(entityType); - var ghostSystemState = chunk.GetNativeArray(ghostSystemStateType); + var ghostSystemState = chunk.GetNativeArray(ref ghostSystemStateType); // First figure out the baselines to use per entity so they can be sent as baseline + maxCount instead of one per entity int irrelevantCount = 0; for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent) @@ -1259,8 +1294,8 @@ int UpdateGhostRelevancy(ArchetypeChunk chunk, int startIndex, byte* relevancyDa } int UpdateValidGhostGroupRelevancy(ArchetypeChunk chunk, int startIndex, byte* relevancyData, bool keepState) { - var ghost = chunk.GetNativeArray(ghostComponentType); - var ghostGroupAccessor = chunk.GetBufferAccessor(ghostGroupType); + var ghost = chunk.GetNativeArray(ref ghostComponentType); + var ghostGroupAccessor = chunk.GetBufferAccessor(ref ghostGroupType); int irrelevantCount = 0; for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent) @@ -1293,7 +1328,7 @@ int UpdateValidGhostGroupRelevancy(ArchetypeChunk chunk, int startIndex, byte* r var ackTick = snapshotAck.LastReceivedSnapshotByRemote; var zeroChangeTick = chunkState.GetFirstZeroChangeTick(); var zeroChangeVersion = chunkState.GetFirstZeroChangeVersion(); - var hasPrespawn = chunk.Has(prespawnBaselineTypeHandle); + var hasPrespawn = chunk.Has(ref prespawnBaselineTypeHandle); //Zero change version is 0 if: // - structural changes are present and the chunk is not a pre-spawn chunk @@ -1322,7 +1357,7 @@ int UpdateValidGhostGroupRelevancy(ArchetypeChunk chunk, int startIndex, byte* r //For prespawn check if we never sent any of entities. If that is the case we can still use static opt if (!canSkipZeroChange && hasPrespawn) { - var systemStates = chunk.GetNativeArray(ghostSystemStateType); + var systemStates = chunk.GetNativeArray(ref ghostSystemStateType); canSkipZeroChange = true; for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount && canSkipZeroChange; ++i) canSkipZeroChange = (ghostStateData.GetGhostState(systemStates[i]).Flags & ConnectionStateData.GhostStateFlags.SentWithChanges) == 0; @@ -1336,14 +1371,14 @@ int UpdateValidGhostGroupRelevancy(ArchetypeChunk chunk, int startIndex, byte* r { int compIdx = GhostComponentIndex[baseOffset + i].ComponentIndex; ValidateGhostComponentIndex(compIdx); - if (chunk.DidChange(ghostChunkComponentTypesPtr[compIdx], zeroChangeVersion)) + if (chunk.DidChange(ref ghostChunkComponentTypesPtr[compIdx], zeroChangeVersion)) return false; } return true; } private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, GhostChunkSerializationState curChunkState, int snapshotSize) { - var ghostSystemState = currentChunk.GetNativeArray(ghostSystemStateType); + var ghostSystemState = currentChunk.GetNativeArray(ref ghostSystemStateType); var ghostEntities = currentChunk.GetNativeArray(entityType); NativeParallelHashMap prevSnapshots = default; for (int currentIndexInChunk = 0, chunkEntityCount = currentChunk.Count; currentIndexInChunk < chunkEntityCount; ++currentIndexInChunk) @@ -1419,7 +1454,7 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos } } } - public bool SerializeChunk(in GhostSendSystem.PrioChunk serialChunk, ref DataStreamWriter dataStream, + public SerializeEnitiesResult SerializeChunk(in GhostSendSystem.PrioChunk serialChunk, ref DataStreamWriter dataStream, ref uint updateLen, ref bool didFillPacket) { int entitySize = UnsafeUtility.SizeOf(); @@ -1453,8 +1488,8 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos chunkState.SetOrderChangeVersion(chunk.GetOrderVersion()); //For prespawn the first zero-change tick is 0 and the version is going to be equals to the change version of of the PrespawnBaseline buffer. //Structural changes in the chunck does not invalidate the baselines for the prespawn (since the buffer is store per entity). - if (chunk.Has(prespawnBaselineTypeHandle)) - chunkState.SetFirstZeroChange(NetworkTick.Invalid, chunk.GetChangeVersion(prespawnBaselineTypeHandle)); + if (chunk.Has(ref prespawnBaselineTypeHandle)) + chunkState.SetFirstZeroChange(NetworkTick.Invalid, chunk.GetChangeVersion(ref prespawnBaselineTypeHandle)); else chunkState.SetFirstZeroChange(NetworkTick.Invalid, 0); PacketDumpStructuralChange(serialChunk.ghostType); @@ -1491,7 +1526,7 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos // This happens when using relevancy and on structural changes while there is a partially sent chunk // We update the timestamp as if the chunk was sent but do not actually send anything chunkState.SetLastUpdate(currentTick); - return true; + return SerializeEnitiesResult.Ok; } // Only apply the zero change optimization for ghosts tagged as optimize for static @@ -1506,7 +1541,7 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos { // There were not changes we required to send, treat is as if we did send the chunk to make sure we do not collect all static chunks as the top priority ones chunkState.SetLastUpdate(currentTick); - return true; + return SerializeEnitiesResult.Ok; } } @@ -1527,7 +1562,7 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos else if (relevancyEnabled || GhostTypeCollection[ghostType].NumBuffers > 0 || GhostTypeCollection[ghostType].IsGhostGroup != 0) // Do not send ghosts which were just created since they have not had a chance to be added to the relevancy set yet // Do not send ghosts which were just created and have buffers, mostly to simplify the dynamic buffer size calculations - return true; + return SerializeEnitiesResult.Ok; int ent; @@ -1540,13 +1575,13 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos dataStream.WritePackedUInt((uint) relevantGhostCount, compressionModel); // Write 1 bits for that run if the entity are pre-spawned objects. This will change how the ghostId // is encoded and will not write the spawn tick - dataStream.WriteRawBits(chunk.Has(PrespawnIndexType)?1u:0u, 1); + dataStream.WriteRawBits(chunk.Has(ref PrespawnIndexType)?1u:0u, 1); PacketDumpGhostCount(ghostType, relevantGhostCount); if (dataStream.HasFailedWrites) { dataStream = oldStream; didFillPacket = true; - return false; + return SerializeEnitiesResult.Failed; } GhostTypeCollection[ghostType].profilerMarker.Begin(); @@ -1560,7 +1595,7 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos // Do not send partial chunks for zero changes unless we have to since the zero change optimizations only kick in if the full chunk was sent dataStream = oldStream; didFillPacket = true; - return false; + return SerializeEnitiesResult.Failed; } currentChunkUpdateLen = (uint) (ent - serialChunk.startIndex - skippedEntityCount); @@ -1577,7 +1612,7 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos } // Spawn chunks are temporary and should not be added to the state data cache - if (chunk.Has(ghostSystemStateType)) + if (chunk.Has(ref ghostSystemStateType)) { // Only append chunks which contain data, and only update the write index if we actually sent it if (currentChunkUpdateLen > 0 && !(isZeroChange && canSkipZeroChange)) @@ -1618,9 +1653,9 @@ private void UpdateChunkHistory(int ghostType, ArchetypeChunk currentChunk, Ghos if (ent < chunk.Count) { didFillPacket = true; - return false; + return SerializeEnitiesResult.Failed; } - return true; + return SerializeEnitiesResult.Ok; } } } diff --git a/Runtime/Snapshot/GhostCollectionComponent.cs b/Runtime/Snapshot/GhostCollectionComponent.cs index 5fec5f8..06ed42f 100644 --- a/Runtime/Snapshot/GhostCollectionComponent.cs +++ b/Runtime/Snapshot/GhostCollectionComponent.cs @@ -133,11 +133,9 @@ public struct GhostCollection : IComponentData /// /// The list is sorted by the value of the guid. /// - [InternalBufferCapacity(ghostCollectionPrefabBufferSize)] + [InternalBufferCapacity(0)] public struct GhostCollectionPrefab : IBufferElementData { - internal const int ghostCollectionPrefabBufferSize = 96; - /// /// Ghost prefabs can be added dynamically to the ghost collection as soon as they are loaded from either a /// sub-scene, or created dynamically at runtime. @@ -186,7 +184,7 @@ public enum LoadingState /// serializers are created yet. /// Added to the GhostCollection singleton entity. /// - [InternalBufferCapacity(GhostCollectionPrefab.ghostCollectionPrefabBufferSize)] + [InternalBufferCapacity(0)] internal struct GhostCollectionPrefabSerializer : IBufferElementData { /// @@ -259,7 +257,7 @@ internal struct GhostCollectionPrefabSerializer : IBufferElementData /// public int IsGhostGroup; /// - /// The number of bits necessary to store the enabled state of all the enableable ghost components. + /// The number of bits necessary to store the enabled state of all the enableable ghost components (that are flagged with ). /// public int EnableableBits; /// @@ -282,7 +280,7 @@ internal struct GhostCollectionPrefabSerializer : IBufferElementData /// to a concrete ComponentType in jobs. /// Added to the GhostCollection singleton entity. /// - [InternalBufferCapacity(128)] + [InternalBufferCapacity(0)] internal struct GhostCollectionComponentType : IBufferElementData { /// @@ -305,7 +303,7 @@ internal struct GhostCollectionComponentType : IBufferElementData /// to use from this array. /// Added to the GhostCollection singleton entity. /// - [InternalBufferCapacity(128)] + [InternalBufferCapacity(0)] internal struct GhostCollectionComponentIndex : IBufferElementData { /// Index of ghost entity the rule applies to. diff --git a/Runtime/Snapshot/GhostCollectionSystem.cs b/Runtime/Snapshot/GhostCollectionSystem.cs index 6257e2f..b80d5cf 100644 --- a/Runtime/Snapshot/GhostCollectionSystem.cs +++ b/Runtime/Snapshot/GhostCollectionSystem.cs @@ -3,15 +3,14 @@ #endif using System; -using System.Diagnostics; using Unity.Entities; using Unity.Collections; using Unity.NetCode.LowLevel.Unsafe; using System.Collections.Generic; -using Unity.Assertions; using Unity.Burst; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; +using Unity.NetCode.LowLevel; using Unity.Profiling; namespace Unity.NetCode @@ -86,16 +85,15 @@ private struct UsedComponentType public GhostCollectionComponentType ComponentType; } private NativeList m_AllComponentTypes; - //retrieve the index inside the GhostCollectionComponentIndex for a component, given its stable hash. + /// Retrieve the index inside the GhostCollectionComponentIndex for a component, given its stable hash. private NativeHashMap m_StableHashToComponentTypeIndex; - private Entity m_CodePrefabSingleton; EntityQuery m_RegisterGhostTypesQuery; //Hash requirements: // R0: if components are different or in different order the hash should change - // R1: different size, owneroffsets, maskbits, partialcomponents etc must result in a different hash - // R2: if a ghost present the same components, with the same fields but different [GhostField] attributes (such as, subType, interpoled, composite) + // R1: different size, owner offsets, mask bits, partial components etc must result in a different hash + // R2: if a ghost present the same components, with the same fields but different [GhostField] attributes (such as, subType, interpolated, composite) // must result in a different hash, even though the resulting serialization sizes and masks are the same internal static ulong CalculateComponentCollectionHash(DynamicBuffer ghostComponentCollection) { @@ -157,12 +155,14 @@ public void OnCreate(ref SystemState state) m_RuntimeStripQuery = state.GetEntityQuery(entityQueryBuilder); state.RequireForUpdate(); + // TODO - Deduplicate this data by removing all unnecessary buffers. m_CollectionSingleton = state.EntityManager.CreateSingleton("Ghost Collection"); state.EntityManager.AddBuffer(m_CollectionSingleton); state.EntityManager.AddBuffer(m_CollectionSingleton); state.EntityManager.AddBuffer(m_CollectionSingleton); state.EntityManager.AddBuffer(m_CollectionSingleton); state.EntityManager.AddBuffer(m_CollectionSingleton); + state.EntityManager.AddComponent(m_CollectionSingleton); #if UNITY_EDITOR || DEVELOPMENT_BUILD m_PredictionErrorNames = new NativeList(16, Allocator.Persistent); @@ -338,20 +338,17 @@ public void OnUpdate(ref SystemState state) } } - var prefabSerializerCollection = state.EntityManager.GetBuffer(m_CollectionSingleton); - var ctx = new AddComponentCtx { ghostPrefabCollection = state.EntityManager.GetBuffer(m_CollectionSingleton), ghostSerializerCollection = state.EntityManager.GetBuffer(m_CollectionSingleton), - ghostPrefabSerializerCollection = prefabSerializerCollection, + ghostPrefabSerializerCollection = state.EntityManager.GetBuffer(m_CollectionSingleton), ghostComponentCollection = state.EntityManager.GetBuffer(m_CollectionSingleton), ghostComponentIndex = state.EntityManager.GetBuffer(m_CollectionSingleton), netDebug = netDebug, }; var data = SystemAPI.GetSingletonRW().ValueRW; - for (int i = ctx.ghostPrefabSerializerCollection.Length; i < ctx.ghostPrefabCollection.Length; ++i) { var ghost = ctx.ghostPrefabCollection[i]; @@ -391,6 +388,8 @@ public void OnUpdate(ref SystemState state) } ghost.Hash = hash; #if ENABLE_UNITY_COLLECTIONS_CHECKS + //FIXME the SharedGhostTypeComponent should be always valid (never equals 0:0:0:0) and in general + //equals to the GhostTypeComponent if (state.EntityManager.HasComponent(ghost.GhostPrefab) && state.EntityManager.GetSharedComponent(ghost.GhostPrefab).SharedValue == default) { @@ -399,7 +398,6 @@ public void OnUpdate(ref SystemState state) #endif ctx.ghostPrefabCollection[i] = ghost; } - #if UNITY_EDITOR || DEVELOPMENT_BUILD if (m_PrevPredictionErrorNamesCount < m_currentPredictionErrorNamesCount || m_PrevGhostNamesCount < m_GhostNames.Length) { @@ -569,9 +567,8 @@ private void ProcessGhostPrefab(ref SystemState state, ref GhostComponentSeriali ghostType.PredictionOwnerOffset += GhostComponentSerializer.SnapshotSizeAligned(sizeof(uint) + GhostComponentSerializer.ChangeMaskArraySizeInUInts(ghostType.ChangeMaskBits)*sizeof(uint) + GhostComponentSerializer.ChangeMaskArraySizeInUInts(ghostType.EnableableBits)*sizeof(uint)); } // Reserve space for tick and change mask in the snapshot - - var enabledBitsInBytes = GhostComponentSerializer.ChangeMaskArraySizeInUInts(ghostType.EnableableBits) * sizeof(uint); - var changeMaskBitsInBytes = GhostComponentSerializer.ChangeMaskArraySizeInUInts(ghostType.ChangeMaskBits) * sizeof(uint); + var enabledBitsInBytes = GhostComponentSerializer.ChangeMaskArraySizeInBytes(ghostType.EnableableBits); + var changeMaskBitsInBytes = GhostComponentSerializer.ChangeMaskArraySizeInBytes(ghostType.ChangeMaskBits); ghostType.SnapshotSize += GhostComponentSerializer.SnapshotSizeAligned(sizeof(uint) + changeMaskBitsInBytes + enabledBitsInBytes); ctx.ghostPrefabSerializerCollection.Add(ghostType); @@ -636,35 +633,44 @@ private void RuntimeStripPrefabs(ref SystemState state, in NetDebug netDebug) private void CreateComponentCollection(ref SystemState state) { var data = SystemAPI.GetSingletonRW().ValueRW; + data.Validate(); - var ghostComponentCollectionCount = data.GhostComponentCollection.Count(); + var ghostComponentCollectionCount = data.Serializers.Length; m_StableHashToComponentTypeIndex = new NativeHashMap(ghostComponentCollectionCount, Allocator.Persistent); - var tempGhostComponentCollectionArray = data.GhostComponentCollection.GetValueArray(Allocator.Temp); - tempGhostComponentCollectionArray.Sort(default(ComponentHashComparer)); + // Sort and remap Serializers to their SerializationStrategies. + data.Serializers.Sort(default(ComponentHashComparer)); + for (var i = 0; i < data.Serializers.Length; i++) + { + ref var stateToRemap = ref data.Serializers.ElementAt(i); + stateToRemap.SerializationStrategyIndex = -1; + data.MapSerializerToStrategy(ref stateToRemap, (short) i); + } + + data.Validate(); // Populate the ghost serializer collection buffer with all states. var ghostSerializerCollection = state.EntityManager.GetBuffer(m_CollectionSingleton); ghostSerializerCollection.Clear(); - ghostSerializerCollection.AddRange(tempGhostComponentCollectionArray); + ghostSerializerCollection.AddRange(data.Serializers.AsArray()); // Reset & resize the following buffer so that we can write into it later. { var ghostComponentCollection = state.EntityManager.GetBuffer(m_CollectionSingleton); ghostComponentCollection.Clear(); - ghostComponentCollection.Capacity = tempGhostComponentCollectionArray.Length; + ghostComponentCollection.Capacity = data.Serializers.Length; } // Create the unique list of component types that provide an inverse mapping into the ghost serializer list. - m_AllComponentTypes = new NativeList(tempGhostComponentCollectionArray.Length, Allocator.Persistent); - for (int i = 0; i < tempGhostComponentCollectionArray.Length;) + m_AllComponentTypes = new NativeList(data.Serializers.Length, Allocator.Persistent); + for (int i = 0; i < data.Serializers.Length;) { int firstSerializer = i; - var compType = tempGhostComponentCollectionArray[i].ComponentType; + var compType = data.Serializers[i].ComponentType; do { ++i; - } while (i < tempGhostComponentCollectionArray.Length && tempGhostComponentCollectionArray[i].ComponentType == compType); + } while (i < data.Serializers.Length && data.Serializers[i].ComponentType == compType); m_AllComponentTypes.Add(new UsedComponentType { UsedIndex = -1, @@ -678,14 +684,14 @@ private void CreateComponentCollection(ref SystemState state) m_StableHashToComponentTypeIndex.Add(TypeManager.GetTypeInfo(compType.TypeIndex).StableTypeHash, m_AllComponentTypes.Length - 1); } - //This list does not depend on the number of prefabs but only on the number of serializers avaialble in the project. + + //This list does not depend on the number of prefabs but only on the number of serializers avaialable in the project. //The construction time is linear in number of predicted fields, instead of becoming "quadratic" (number of prefabs x number of predicted fields) #if UNITY_EDITOR || DEVELOPMENT_BUILD PrecomputeComponentErrorNameList(ref ghostSerializerCollection); #endif m_ComponentCollectionInitialized = 1; data.CollectionInitialized = 1; - tempGhostComponentCollectionArray.Dispose(); } private unsafe void AddComponents(ref AddComponentCtx ctx, ref GhostComponentSerializerCollectionData data, ref GhostPrefabMetaData ghostMeta, ref GhostCollectionPrefabSerializer ghostType) @@ -701,22 +707,21 @@ private unsafe void AddComponents(ref AddComponentCtx ctx, ref GhostComponentSer if (isRoot && componentInfo.StableHash == ghostOwnerHash) ghostType.PredictionOwnerOffset = ghostType.SnapshotSize; - if(!m_StableHashToComponentTypeIndex.TryGetValue(componentInfo.StableHash, out var componentIndex)) + if (!m_StableHashToComponentTypeIndex.TryGetValue(componentInfo.StableHash, out var componentIndex)) continue; ref var usedComponent = ref allComponentTypes.ElementAt(componentIndex); var type = usedComponent.ComponentType.Type; - var variant = data.GetCurrentVariantTypeForComponentCached(type, componentInfo.Variant, isRoot); + var variant = data.GetCurrentSerializationStrategyForComponentCached(type, componentInfo.Variant, isRoot); + // Skip component if client only or don't send variants are selected. // This handles children that shouldn't be serialized too. - if (!variant.IsSerialized) - { + if (variant.IsSerialized == 0) continue; - } //The search is sub-linear, since this is a sort of multi-hashmap (O(1) on average), but the //cache misses (component indices) are random are the dominating factor. - int serializerIndex = usedComponent.ComponentType.FirstSerializer; + var serializerIndex = usedComponent.ComponentType.FirstSerializer; while (serializerIndex <= usedComponent.ComponentType.LastSerializer && ctx.ghostSerializerCollection.ElementAt(serializerIndex).VariantHash != variant.Hash) ++serializerIndex; @@ -726,7 +731,7 @@ private unsafe void AddComponents(ref AddComponentCtx ctx, ref GhostComponentSer { FixedString512Bytes errorMsg = $"Cannot find serializer for componentIndex {componentIndex} with componentInfo (with variant: '{componentInfo.Variant}') with '{variant}' type returned, for ghost '"; errorMsg.Append(ctx.ghostName); - errorMsg.Append((FixedString128Bytes)$"'. serializerIndex: {serializerIndex} vs f:{allComponentTypes[componentIndex].ComponentType.FirstSerializer} l:{allComponentTypes[componentIndex].ComponentType.LastSerializer}!"); + errorMsg.Append((FixedString128Bytes) $"'. serializerIndex: {serializerIndex} vs f:{allComponentTypes[componentIndex].ComponentType.FirstSerializer} l:{allComponentTypes[componentIndex].ComponentType.LastSerializer}!"); ctx.netDebug.LogError(errorMsg); return; } @@ -734,6 +739,7 @@ private unsafe void AddComponents(ref AddComponentCtx ctx, ref GhostComponentSer //Apply prefab overrides if any ref var compState = ref ctx.ghostSerializerCollection.ElementAt(serializerIndex); + var sendMask = componentInfo.SendMaskOverride >= 0 ? (GhostComponentSerializer.SendMask) componentInfo.SendMaskOverride : compState.SendMask; @@ -764,7 +770,8 @@ private unsafe void AddComponents(ref AddComponentCtx ctx, ref GhostComponentSer ghostType.MaxBufferSnapshotSize = math.max(compState.SnapshotSize, ghostType.MaxBufferSnapshotSize); ++ghostType.NumBuffers; } - ghostType.EnableableBits += type.IsEnableable ? 1:0; + + ghostType.EnableableBits += compState.SerializesEnabledBit; // 1 = true, 0 = false; implicit map to counter here. // Make sure the component is now in use if (usedComponent.UsedIndex < 0) @@ -833,15 +840,15 @@ public PendingNameAssignment(int nameIndex, int childIndex, int serializer) private void ProcessPendingNameAssignments(DynamicBuffer ghostComponentSerializers) { - int appendIndex = m_PredictionErrorNames.Length; + var appendIndex = m_PredictionErrorNames.Length; Assertions.Assert.IsTrue(m_currentPredictionErrorNamesCount > m_PredictionErrorNames.Length); m_PredictionErrorNames.ResizeUninitialized(m_currentPredictionErrorNamesCount); foreach (var nameToAssign in m_PendingNameAssignments) { ref var ghostName = ref m_GhostNames.ElementAt(nameToAssign.ghostName); ref var compState = ref ghostComponentSerializers.ElementAt(nameToAssign.serializerIndex); - int ghostChildIndex = nameToAssign.ghostChildIndex; - for (int i = 0; i < compState.NumPredictionErrorNames; ++i) + var ghostChildIndex = nameToAssign.ghostChildIndex; + for (var i = 0; i < compState.NumPredictionErrorNames; ++i) { ref var errorName = ref m_PredictionErrorNames.ElementAt(appendIndex).Name; var compStartEnd = m_PredictionErrorNamesStartEndCache[compState.FirstNameIndex + i]; @@ -911,7 +918,7 @@ private void PrecomputeComponentErrorNameList(ref DynamicBuffer + /// Construct a new from a guid string. + /// + /// a guid string. Either Hash128 or Unity.Engine.GUID strings are valid. + /// a new GhostTypeComponent instance + [BurstDiscard] + internal static GhostTypeComponent FromHash128String(string guid) + { + var hash = new Hash128(guid); + return new GhostTypeComponent + { + guid0 = hash.Value.x, + guid1 = hash.Value.y, + guid2 = hash.Value.z, + guid3 = hash.Value.w, + }; + } + + /// + /// Create a new from the give guid. + /// + /// + /// + internal static GhostTypeComponent FromHash128(Hash128 guid) + { + return new GhostTypeComponent + { + guid0 = guid.Value.x, + guid1 = guid.Value.y, + guid2 = guid.Value.z, + guid3 = guid.Value.w, + }; + } + + /// + /// Convert a to a instance. The hash will always match the prefab guid + /// from which the ghost has been created. + /// + /// + /// + public static explicit operator Hash128(GhostTypeComponent ghostType) + { + return new Hash128(ghostType.guid0, ghostType.guid1, ghostType.guid2, ghostType.guid3); + + } + /// /// Returns whether or not two GhostTypeComponent are identical. /// diff --git a/Runtime/Snapshot/GhostComponentSerializer.cs b/Runtime/Snapshot/GhostComponentSerializer.cs index fc2365b..9200b99 100644 --- a/Runtime/Snapshot/GhostComponentSerializer.cs +++ b/Runtime/Snapshot/GhostComponentSerializer.cs @@ -8,10 +8,10 @@ namespace Unity.NetCode { /// /// For internal use only. - /// The base class for all the code-generated systems responsible for registering all the generated component + /// The interface for all the code-generated ISystems responsible for registering all the generated component /// serializers into the . /// - public abstract partial class GhostComponentSerializerRegistrationSystemBase : SystemBase + public interface IGhostComponentSerializerRegistration {} } @@ -123,7 +123,7 @@ public struct State : IBufferElementData public ulong GhostFieldsHash; /// /// An hash identifying the specific variation used for this serializer (see ). - /// If not variation is used, this will be the hash of the itself, and will be true. + /// If no variation is used, this will be the hash of the itself, and will be true. /// public ulong VariantHash; /// @@ -131,9 +131,9 @@ public struct State : IBufferElementData /// public ComponentType ComponentType; /// - /// Internal, the index inside the list. + /// Internal. Indexer into the list. /// - public int VariantTypeIndex; + public short SerializationStrategyIndex; /// /// The size of the component, as reported by the . /// @@ -143,12 +143,20 @@ public struct State : IBufferElementData /// public int SnapshotSize; /// + /// Whether SnapshotSize is greater than zero. + /// + public bool HasGhostFields => SnapshotSize > 0; + /// /// The number of bits necessary for the change mask. /// public int ChangeMaskBits; + /// True if this component has the and thus should replicate the enable bit flag. + /// Note that serializing the enabled bit is different from the main "serializer". I.e. "Empty Variants" can have serialized enable bits. + public byte SerializesEnabledBit; /// /// Store the if the attribute is present on the component. Otherwise is set /// to . + /// TODO - Try to deduplicate this data by reading the ComponentTypeSerializationStrategy directly. /// public GhostPrefabType PrefabType; /// @@ -161,19 +169,6 @@ public struct State : IBufferElementData /// to . /// public SendToOwnerType SendToOwner; - - private byte _SendForChildEntities; - /// True if the flag is true on this variant (if it has one), or this type (if not). - public bool SendForChildEntities { get { return _SendForChildEntities != 0; } set { _SendForChildEntities = (byte)(value ? 1 : 0); } } - - private byte _IsDefaultSerializer; - /// - /// True if this is the "default" serializer for this component type. - /// I.e. The one generated from the component definition itself (see and ). - /// - /// Types like `Translation` don't have a default serializer as the type itself doesn't define any GhostFields, but they do have serialized variants. - public bool IsDefaultSerializer { get { return _IsDefaultSerializer != 0; } set { _IsDefaultSerializer = (byte)(value ? 1 : 0); } } - /// /// Delegate method to use to post-serialize the component when the ghost use pre-serialization optimization. /// @@ -250,17 +245,6 @@ public struct State : IBufferElementData #endif } - /// - /// The list of all serialized component and variant () types. - /// Populated at runtime when the generated serialized are registered to collection. - /// (see also ). - /// - public static List VariantTypes = new List(64) - { - typeof(DontSerializeVariant), - typeof(ClientOnlyVariant), - }; - /// /// Helper that returns the size in bytes (aligned to 16 bytes boundary) used to store the component data inside . /// @@ -369,7 +353,7 @@ internal static int SnapshotHeaderSizeInBytes(in GhostCollectionPrefabSerializer /// Compute the number of uint necessary to encode the required number of bits /// /// - /// + /// The uint mask to encode this number of bits. public static int ChangeMaskArraySizeInUInts(int numBits) { return (numBits + 31)>>5; @@ -379,14 +363,14 @@ public static int ChangeMaskArraySizeInUInts(int numBits) /// Compute the number of bytes necessary to encode the required number of bits /// /// - /// + /// The min number of bytes to store this number of bits, rounded to the nearest 4 bytes (for data-alignment). public static int ChangeMaskArraySizeInBytes(int numBits) { return ((numBits + 31)>>3) & ~0x3; } /// - /// Align the give size to 16 byte boundary + /// Align the give size to 16 byte boundary. /// /// /// @@ -422,7 +406,7 @@ internal static class DynamicBufferExtensions { var ptr = (T*)buffer.GetUnsafeReadOnlyPtr(); #if ENABLE_UNITY_COLLECTIONS_CHECKS - if(index < 0 || index > buffer.Length) + if(index < 0 || index >= buffer.Length) throw new IndexOutOfRangeException($"Index {index} is out of range in DynamicBuffer of '{buffer.Length}' Length."); #endif return ref ptr[index]; diff --git a/Runtime/Snapshot/GhostComponentSerializerCollectionSystemGroup.cs b/Runtime/Snapshot/GhostComponentSerializerCollectionSystemGroup.cs index 869887c..272b7ce 100644 --- a/Runtime/Snapshot/GhostComponentSerializerCollectionSystemGroup.cs +++ b/Runtime/Snapshot/GhostComponentSerializerCollectionSystemGroup.cs @@ -1,184 +1,165 @@ using System; -using System.Collections.Generic; -using System.Reflection; +using System.Diagnostics; using System.Runtime.InteropServices; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.NetCode.LowLevel.Unsafe; -using UnityEngine; namespace Unity.NetCode { /// /// A component - variant - root tuple, - /// used for caching the result + /// used for caching the result /// and speed-up successive query of the same component-variant combination. /// - internal struct VariantQuery : IComparable, IEquatable + internal struct SerializationStrategyQuery : IComparable, IEquatable { public ComponentType ComponentType; public ulong variantHash; + /// + /// 0 = No. + /// 1 = Yes. + /// 2 to 255 = Special cases: Variant added to the map to be searchable. + /// + public byte IsRoot; - private byte _isRoot; - public bool isRoot => _isRoot != 0; - - public VariantQuery(ComponentType type, ulong hash, bool root) + public SerializationStrategyQuery(ComponentType type, ulong hash, byte isRoot) { ComponentType = type; variantHash = hash; - _isRoot = (byte) (root ? 1 : 0); + IsRoot = isRoot; } - public int CompareTo(VariantQuery other) + public int CompareTo(SerializationStrategyQuery other) { var componentTypeComparison = ComponentType.CompareTo(other.ComponentType); if (componentTypeComparison != 0) return componentTypeComparison; var variantHashComparison = variantHash.CompareTo(other.variantHash); if (variantHashComparison != 0) return variantHashComparison; - return isRoot.CompareTo(other.isRoot); + return IsRoot.CompareTo(other.IsRoot); } - public bool Equals(VariantQuery other) + public bool Equals(SerializationStrategyQuery other) { - return ComponentType.Equals(other.ComponentType) && variantHash == other.variantHash && isRoot == other.isRoot; + return ComponentType.Equals(other.ComponentType) && variantHash == other.variantHash && IsRoot == other.IsRoot; } public override bool Equals(object obj) { - return obj is VariantQuery other && Equals(other); + return obj is SerializationStrategyQuery other && Equals(other); } public override int GetHashCode() { var hashCode = ComponentType.GetHashCode(); hashCode = (hashCode * 397) ^ variantHash.GetHashCode(); - hashCode = (hashCode * 397) ^ isRoot.GetHashCode(); + hashCode = (hashCode * 397) ^ IsRoot.GetHashCode(); return hashCode; } } - /// - /// Stores all attribute and reflection data for NetCode GhostComponents and NetCode Variants. - /// Populated via the "Source Generators" code-generation. - /// - public struct CodeGenTypeMetaData - { - /// "Variant Type Hash". Set via . - public ulong TypeHash; - - /// True if the code-generator determined that this is an input component (or a variant of one). - public bool IsInputComponent; - /// True if the code-generator determined that this is an input buffer. - public bool IsInputBuffer; - - /// Does this component explicitly opt-out of overrides (regardless of variant count)? - public bool HasDontSupportPrefabOverridesAttribute; - - /// Does this component explicitly opt-in to overrides (regardless of variant count)? - public bool HasSupportsPrefabOverridesAttribute; - - /// True if this is an editor test variant. Forces this variant to be considered a "default" which makes writing tests easier. - public bool IsTestVariant; - - /// and . - public bool IsInput => IsInputComponent || IsInputBuffer; - - [System.Diagnostics.Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - internal void ThrowIfNoHash() - { - GhostComponentSerializerCollectionData.ThrowIfNoHash(TypeHash, nameof(TypeHash)); - } - } - // TODO - Make internal if possible. /// /// - /// For internal use only. Encapsulates ghost serialization variant (see ) - /// type information. + /// For internal use only. Stores individual "serialization strategies" (and meta-data) for all netcode-informed components, + /// as well as all variants of these components (). + /// Thus, maps to the code-generated ("Default Serializers") as well as + /// all user-created Variants (). + /// This type also stores instances of the and . /// /// - /// The struct is also used as interop struct by code-gen to register "empty" variants (component for which a - /// variant has been declared using the but for which serialization - /// is not generated, i.e: only the attribute is specified in the variant declaration). + /// Note: Serializers are considered "optional". It is perfectly valid for a types "serialization strategy" to be: "Do nothing". + /// An example of this is a component for which a variant has been declared (using the ) + /// but for which serialization is not generated, i.e: the attribute is specified in + /// the base component declaration, but not in a variant. We call these "Empty Variants". /// /// - public struct VariantType : IComparable + /// This type was renamed from "VariantType" for 1.0. + public struct ComponentTypeSerializationStrategy : IComparable { - /// Denotes why this variant is the default (or not). Higher value = more important. - /// This is a flags enum, so there may be multiple reasons why a variant is considered the default. + /// Denotes why this strategy is the default (or not). Higher value = more important. + /// This is a flags enum, so there may be multiple reasons why a strategy is considered the default. [Flags] public enum DefaultType : byte { /// This is not the default. NotDefault = 0, - /// This is a default variant either due to error or no other variants existing. - YesViaFallbackRule = 1 << 1, + /// It's an editor test variant, so we should default to it if we literally don't have any other defaults. + YesAsEditorDefault = 1 << 1, + /// This is the default variant only because we could not find a suitable one. + YesAsIsFallback = 1 << 2, /// Child entities default to . - YesAsIsChildDefaultingToDontSerializeVariant = 1 << 2, - /// It's an editor test variant. - YesAsEditorDefault = 1 << 3, + YesAsIsChildDefaultingToDontSerializeVariant = 1 << 3, /// The default serializer should be used if we're a root. YesAsIsDefaultSerializerAndIsRoot = 1 << 4, /// Yes via . YesAsAttributeAllowingChildSerialization = 1 << 5, - /// If the developer has only specified one variant, it becomes the default. + /// If the developer has created only 1 variant for a type, it becomes the default. YesAsOnlyOneVariantBecomesDefault = 1 << 6, /// This is a default variant because the user has marked it as such via . Highest priority. YesViaUserSpecifiedNamedDefault = 1 << 7, } - /// Denotes the source of the variant. I.e. How was it added to this type? - internal enum VariantSource : byte - { - SourceGeneratorSerializers, - SourceGeneratorEmptyVariants, - ManualClientOnlyVariant, - ManualDefaultSerializer, - ManualDontSerializeVariant, - } - + /// Indexer into list. + public short SelfIndex; + /// Indexes into the . + /// Serializers are optional. Thus, 0 if this type does not serialize component data. + public short SerializerIndex; /// Component that this Variant is associated with. public ComponentType Component; - /// Hash of variant. Should be non-zero by the time it's used in . + /// Hash identifier for the strategy. Should be non-zero by the time it's used in . public ulong Hash; /// /// The value set in present in the variant declaration. /// Some variants modify the serialization rules. Default is /// public GhostPrefabType PrefabType; + ///Override which client type it will be sent to, if we're able to determine. + public GhostSendType SendTypeOptimization; /// public DefaultType DefaultRule; - - private byte _IsDefaultSerializer; - /// True if this variant is actually just the type (thus it's the default serializer). - public bool IsDefaultSerializer { get { return _IsDefaultSerializer != 0; } set { _IsDefaultSerializer = (byte)(value ? 1 : 0); } } - - private byte _IsSerialized; - /// True if this variant serializes its data. - public bool IsSerialized { get { return _IsSerialized != 0; } set { _IsSerialized = (byte)(value ? 1 : 0); } } - - private byte _IsTestVariant; + // TODO - Create a flag byte enum for all of these. + /// + /// True if this is the "default" serializer for this component type. + /// I.e. The one generated from the component definition itself (see and ). + /// + /// Types like `Translation` don't have a default serializer as the type itself doesn't define any GhostFields, but they do have serialized variants. + public byte IsDefaultSerializer; /// - public bool IsTestVariant { get { return _IsTestVariant != 0; } set { _IsTestVariant = (byte)(value ? 1 : 0); } } - - /// - internal VariantSource Source; - /// Lookup into the array. - public int VariantTypeIndex; - - /// The variant type. It'll return the component type itself if not able to resolve. - public Type Variant => VariantTypeIndex >= 0 && VariantTypeIndex < GhostComponentSerializer.VariantTypes.Count ? GhostComponentSerializer.VariantTypes[VariantTypeIndex] : Component.GetManagedType(); + /// True if this is an editor test variant. Forces this variant to be considered a "default" which makes writing tests easier. + public byte IsTestVariant; + /// True if the flag is true on this variant (if it has one), or this type (if not). + public byte SendForChildEntities; + /// True if the code-generator determined that this is an input component (or a variant of one). + public byte IsInputComponent; + /// True if the code-generator determined that this is an input buffer. + public byte IsInputBuffer; + /// Does this component explicitly opt-out of overrides (regardless of variant count)? + public byte HasDontSupportPrefabOverridesAttribute; + /// Does this component explicitly opt-in to overrides (regardless of variant count)? + public byte HasSupportsPrefabOverridesAttribute; + /// and . + internal byte IsInput => (byte) (IsInputComponent | IsInputBuffer); + /// The type name, unless it has a Variant (in which case it'll use the Variant Display name... assuming that is not null). + public FixedString64Bytes DisplayName; + /// True if this variant serializes its data. + /// Note that this will also be true if the type has the attribute . + public byte IsSerialized => (byte) (SerializerIndex >= 0 ? 1 : 0); + /// True if this variant is the . + public bool IsDontSerializeVariant => Hash == GhostVariantsUtility.DontSerializeHash; + /// True if this variant is the . + public bool IsClientOnlyVariant => Hash == GhostVariantsUtility.ClientOnlyHash; /// /// Check if two VariantType are identical. /// /// /// - public int CompareTo(VariantType other) + public int CompareTo(ComponentTypeSerializationStrategy other) { if (IsSerialized != other.IsSerialized) - return !IsSerialized ? -1 : 1; + return IsSerialized - other.IsSerialized; if (DefaultRule != other.DefaultRule) return DefaultRule - other.DefaultRule; if (Hash != other.Hash) @@ -190,34 +171,18 @@ public int CompareTo(VariantType other) /// Convert the instance to its string representation. /// /// - public override string ToString() => $"VT<{Component}>[H:{Hash}, Variant: `{Variant.FullName}` (VTI:'{VariantTypeIndex}'), DR:{DefaultRule}, Serialized:{(IsSerialized ? '1' : '0')}, PT:{PrefabType}, Source:'{Source}']"; - - [BurstDiscard] - void NonBurstedBetterLog(ref FixedString512Bytes fs) => fs.Append(ToString()); + public override string ToString() => ToFixedString().ToString(); /// Logs a burst compatible debug string (if in burst), otherwise logs even more info. /// A debug string. [GenerateTestsForBurstCompatibility] public FixedString512Bytes ToFixedString() { - var fs = new FixedString512Bytes(); - NonBurstedBetterLog(ref fs); - if (fs.Length == 0) - { - var isSerialized = IsSerialized ? 1 : 0; - fs = new FixedString512Bytes((FixedString32Bytes)$"VT<"); - fs.Append(Component.GetDebugTypeName()); - fs.Append((FixedString128Bytes)$">[H:{Hash}, VTI:'{VariantTypeIndex}', DR:{(int)DefaultRule}, Serialized:{isSerialized}, PT:{(int)PrefabType}, Source:'{(int)Source}']"); - } + var fs = new FixedString512Bytes((FixedString32Bytes) $"SS<"); + fs.Append(Component.GetDebugTypeName()); + fs.Append((FixedString128Bytes) $">[{DisplayName}, H:{Hash}, DR:{(int) DefaultRule}, SI:{SerializerIndex}, PT:{(int) PrefabType}, self:{SelfIndex}]"); return fs; } - -#if UNITY_EDITOR - /// Returns a readable name for the variant. - /// Meta-data of this ComponentType. - /// A readable name string. - public string CreateReadableName(CodeGenTypeMetaData metaData) => Variant.GetCustomAttribute()?.DisplayName ?? Variant.Name; -#endif } /// @@ -229,18 +194,18 @@ public FixedString512Bytes ToFixedString() WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.BakingSystem)] public partial class GhostComponentSerializerCollectionSystemGroup : ComponentSystemGroup { - /// Increase this if you have thousands of types. - public static int CollectionCapacity = 1024; - - /// - /// Only required because, in some strange cases, a class derived from (which has a `CreateAfter` attribute) - /// will have its `OnCreate` called before this groups OnCreate. - /// - Dictionary m_DefaultVariantsManaged = new Dictionary(32); + /// HashSets and HashTables have a fixed capacity. + /// Increase this if you have lots of variants. Hardcoded multiplier is due to DontSerializeVariants. + public static int CollectionDefaultCapacity = (int) (DynamicTypeList.MaxCapacity * 2.2); /// Hacky workaround for GetSingleton not working on frame 0 (not sure why, as creation order is correct). internal GhostComponentSerializerCollectionData ghostComponentSerializerCollectionDataCache { get; private set; } + /// + /// Used to store the default ghost component variation mapping during the world creation. + /// + internal GhostVariantRules DefaultVariantRules { get; private set; } + struct NeverCreatedSingleton : IComponentData {} @@ -248,68 +213,29 @@ protected override void OnCreate() { base.OnCreate(); RequireForUpdate(); - - // Convert the managed values into a burst-compatible format. - var defaultVariants = new NativeHashMap(CollectionCapacity, Allocator.Persistent); - - foreach (var kvp in m_DefaultVariantsManaged) - defaultVariants[kvp.Key] = kvp.Value.CreateHashRule(kvp.Key); - var worldNameShortened = new FixedString32Bytes(); FixedStringMethods.CopyFromTruncated(ref worldNameShortened, World.Unmanaged.Name); ghostComponentSerializerCollectionDataCache = new GhostComponentSerializerCollectionData { WorldName = worldNameShortened, - GhostComponentCollection = new NativeMultiHashMap(CollectionCapacity, Allocator.Persistent), - CodeGenTypeMetaData = new NativeHashMap(CollectionCapacity, Allocator.Persistent), - EmptyVariants = new NativeMultiHashMap(CollectionCapacity, Allocator.Persistent), - DefaultVariants = defaultVariants, - TypeVariantCache = new NativeHashMap(CollectionCapacity, Allocator.Persistent), + Serializers = new NativeList(CollectionDefaultCapacity, Allocator.Persistent), + SerializationStrategies = new NativeList(CollectionDefaultCapacity, Allocator.Persistent), + SerializationStrategiesComponentTypeMap = new NativeMultiHashMap(CollectionDefaultCapacity, Allocator.Persistent), + DefaultVariants = new NativeHashMap(CollectionDefaultCapacity, Allocator.Persistent), + SerializationStrategiesCache = new NativeHashMap(CollectionDefaultCapacity, Allocator.Persistent), }; + DefaultVariantRules = new GhostVariantRules(ghostComponentSerializerCollectionDataCache.DefaultVariants); + //ATTENTION! this entity is destroyed in the BakingWorld, because in the first import this is what it does, it clean all the Entities in the world when you + //open a scene. + //For that reason, is the current world is a Baking word. this entity is "lazily" recreated by the GhostAuthoringBakingSystem if missing. EntityManager.CreateSingleton(ghostComponentSerializerCollectionDataCache); } protected override void OnDestroy() { - ghostComponentSerializerCollectionDataCache.GhostComponentCollection.Dispose(); - ghostComponentSerializerCollectionDataCache.CodeGenTypeMetaData.Dispose(); - ghostComponentSerializerCollectionDataCache.EmptyVariants.Dispose(); - ghostComponentSerializerCollectionDataCache.DefaultVariants.Dispose(); - ghostComponentSerializerCollectionDataCache.TypeVariantCache.Dispose(); + ghostComponentSerializerCollectionDataCache.Dispose(); ghostComponentSerializerCollectionDataCache = default; - m_DefaultVariantsManaged = default; - } - - public void AppendUserSpecifiedDefaultVariantsToSystem(Dictionary newDefaultVariants) - { - foreach (var kvp in newDefaultVariants) - { - var componentType = kvp.Key; - var newRule = kvp.Value; - var newRuleHash = newRule.CreateHashRule(componentType); - if (m_DefaultVariantsManaged.TryGetValue(componentType, out var existingRule)) - { - var rulesAreTheSame = existingRule.Equals(newRule); - if (!rulesAreTheSame) - { - var useNew = newRule.GetHashCode() < existingRule.GetHashCode(); - UnityEngine.Debug.LogError($"`{this}` is attempting to add a default variant rule '{newRule}' ('{newRuleHash}') for type `{componentType}` but one already " + - $"exists ('{existingRule}' ('{existingRule.CreateHashRule(componentType)}') in this world ('{World.Name}'), likely from a previous system! Using the rule with the smallest HashCode, which is rule '{(useNew ? newRule : existingRule)}'."); - - if (!useNew) - continue; - } - } - - m_DefaultVariantsManaged[componentType] = newRule; - } - - var cache = ghostComponentSerializerCollectionDataCache; - if (cache.DefaultVariants.IsCreated) - { - foreach (var kvp in newDefaultVariants) - cache.DefaultVariants[kvp.Key] = kvp.Value.CreateHashRule(kvp.Key); - } + DefaultVariantRules = null; } } @@ -318,11 +244,29 @@ public void AppendUserSpecifiedDefaultVariantsToSystem(Dictionary GhostComponentCollection; + /// + /// All the Serializers. Allows us to serialize 's to the snapshot. + /// + internal NativeList Serializers; + /// + /// Stores all known code-forced default variants. + /// internal NativeHashMap DefaultVariants; - internal NativeMultiHashMap EmptyVariants; - internal NativeHashMap CodeGenTypeMetaData; - internal NativeHashMap TypeVariantCache; + /// + /// Every netcode-related ComponentType needs a "strategy" for serializing it. This stores all of them. + /// + internal NativeList SerializationStrategies; + /// + /// Cache and lookup into the list for the call. + /// + internal NativeHashMap SerializationStrategiesCache; + /// + /// Maps a given to an entry in the collection. + /// + internal NativeMultiHashMap SerializationStrategiesComponentTypeMap; + /// + /// For debugging and exception strings. + /// internal FixedString32Bytes WorldName; ulong HashGhostComponentSerializer(in GhostComponentSerializer.State comp) @@ -345,11 +289,33 @@ ulong HashGhostComponentSerializer(in GhostComponentSerializer.State comp) /// Used by code-generated systems and meant For internal use only. /// Register an empty variant to the empty variants list. /// - /// - public void AddEmptyVariant(VariantType variantType) + /// + public void AddSerializationStrategy(ref ComponentTypeSerializationStrategy serializationStrategy) { - ThrowIfNoHash(variantType.Hash, $"AddEmptyVariant for '{variantType}'"); - EmptyVariants.Add(variantType.Component, variantType); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + ThrowIfNoHash(serializationStrategy.Hash, serializationStrategy.ToFixedString()); + if (serializationStrategy.DisplayName.IsEmpty) + { + UnityEngine.Debug.LogError($"{serializationStrategy.ToFixedString()} doesn't have a valid DisplayName! Ensure you set it, even if it's just to the ComponentType name."); + serializationStrategy.DisplayName.CopyFromTruncated(serializationStrategy.Component.ToFixedString()); + } + + foreach (var existingSSIndex in SerializationStrategiesComponentTypeMap.GetValuesForKey(serializationStrategy.Component)) + { + var existingSs = SerializationStrategies[existingSSIndex]; + if (existingSs.Hash == serializationStrategy.Hash || existingSs.DisplayName == serializationStrategy.DisplayName) + { + UnityEngine.Debug.LogError($"{serializationStrategy.ToFixedString()} has the same Hash or DisplayName as already-added one (below)! Likely error in code-generation, must fix!\n{existingSs.ToFixedString()}!"); + } + } +#endif + + serializationStrategy.SelfIndex = (short)SerializationStrategies.Length; + SerializationStrategies.Add(serializationStrategy); + SerializationStrategiesComponentTypeMap.Add(serializationStrategy.Component, serializationStrategy.SelfIndex); + + //if (serializationStrategy.SerializeEnabledBit != 0) + // GhostComponentsWithReplicatedEnabledBit.Add(serializationStrategy.Component); } /// @@ -362,136 +328,141 @@ public void AddSerializer(GhostComponentSerializer.State state) //This is always enforced to avoid bad usage of the api if (CollectionInitialized != 0) { - throw new InvalidOperationException($"'{WorldName}': Cannot register new GhostComponentSerializer for `{GetType().FullName}` after the RpcSystem has started running!"); + throw new InvalidOperationException($"'{WorldName}': Cannot register new GhostComponentSerializer for type {state.ComponentType} after the RpcSystem has started running!"); } #if ENABLE_UNITY_COLLECTIONS_CHECKS - foreach (var existing in GhostComponentCollection.GetValuesForKey(state.ComponentType)) - { - if (existing.VariantHash == state.VariantHash) - { - throw new InvalidOperationException($"'{WorldName}': GhostComponentSerializer for `{GetType().FullName}` is already registered for type {state.ComponentType} and variant {state.VariantHash}!"); - } - } - ThrowIfNoHash(state.VariantHash, $"'{WorldName}': AddSerializer for '{state.ComponentType}'."); #endif + // Map to SerializationStrategy: + MapSerializerToStrategy(ref state, (short) Serializers.Length); state.SerializerHash = HashGhostComponentSerializer(state); - GhostComponentCollection.Add(state.ComponentType, state); + Serializers.Add(state); } - /// - /// Used by code-generated systems and meant for internal use only. - /// Add reflection meta-data for a "variant type hash". - /// - /// Note that it's valid that this may clobber existing values. - /// This is due to a quirk with source-generators: Multiple assemblies can define a variant for the same type (e.g. Translation) - /// thus they'll both attempt to add meta-data for the Translation. A little wasteful, but generally harmless. - /// - public void AddCodeGenTypeMetaData(CodeGenTypeMetaData metaData) + internal unsafe void MapSerializerToStrategy(ref GhostComponentSerializer.State state, short serializerIndex) { - CodeGenTypeMetaData[metaData.TypeHash] = metaData; + foreach (var ssIndex in SerializationStrategiesComponentTypeMap.GetValuesForKey(state.ComponentType)) + { + ref var ss = ref SerializationStrategies.ElementAt(ssIndex); + if (ss.Hash == state.VariantHash) + { + state.SerializationStrategyIndex = ssIndex; + ss.SerializerIndex = serializerIndex; + return; + } + } + + throw new InvalidOperationException($"No SerializationStrategy found for Serializer with Hash: {state.VariantHash}!"); } /// - /// Finds the current variant for this ComponentType via , and updates + /// Finds the current variant for this ComponentType via , and updates /// the internal variant query cache to speed-up subsequent queries. /// [GenerateTestsForBurstCompatibility] [BurstCompile] - internal VariantType GetCurrentVariantTypeForComponentCached(ComponentType componentType, ulong variantHash, bool isRoot) + internal ComponentTypeSerializationStrategy GetCurrentSerializationStrategyForComponentCached(ComponentType componentType, ulong variantHash, bool isRoot) { - var t = new VariantQuery(componentType, variantHash, isRoot); - if (!TypeVariantCache.TryGetValue(t, out var variantType)) + var q = new SerializationStrategyQuery(componentType, variantHash, (byte) (isRoot ? 1 : 0)); + if (!SerializationStrategiesCache.TryGetValue(q, out var ssIndex)) { - variantType = GetCurrentVariantTypeForComponentInternal(componentType, variantHash, isRoot); - TypeVariantCache.Add(t, variantType); + var serializationStrategy = GetCurrentSerializationStrategyForComponentInternal(componentType, variantHash, isRoot); + ssIndex = serializationStrategy.SelfIndex; + SerializationStrategiesCache.Add(q, ssIndex); } + if(ssIndex < 0) + BurstCompatibleErrorWithAggregate(componentType, default, $"{componentType.GetDebugTypeName()} is -1!"); - return variantType; + return SerializationStrategies[ssIndex]; } /// - /// Finds the current variant for this ComponentType using available variants via . - /// If this is a component on a root entity, defaults to the default serializer, otherwise defaults to . + /// Finds the current variant for this ComponentType using available variants via . /// - VariantType GetCurrentVariantTypeForComponentInternal(ComponentType componentType, ulong variantHash, bool isRoot) + /// The type we're finding the SS for. + /// The hash to use to lookup with. 0 implies "use default", + /// which is the default serializer for components on the root entity, + /// and for components on children. + /// True if the entity is a root entity, false if it's a child. + /// This distinction is because child entities default to . + ComponentTypeSerializationStrategy GetCurrentSerializationStrategyForComponentInternal(ComponentType componentType, ulong variantHash, bool isRoot) { - using var available = GetAllAvailableVariantsForType(componentType, isRoot); - return GetCurrentVariantTypeForComponent(componentType, variantHash, in available, isRoot); + using var available = GetAllAvailableSerializationStrategiesForType(componentType, isRoot); + return SelectSerializationStrategyForComponentWithHash(componentType, variantHash, in available, isRoot); } - /// + /// [GenerateTestsForBurstCompatibility] - internal VariantType GetCurrentVariantTypeForComponent(ComponentType componentType, ulong variantHash, in NativeList available, bool isRoot) + internal ComponentTypeSerializationStrategy SelectSerializationStrategyForComponentWithHash(ComponentType componentType, ulong serializationStrategyHash, in NativeList available, bool isRoot) { if (available.Length != 0) { - if (variantHash == 0) + if (serializationStrategyHash == 0) { - // Find the best default variant: + // Find the best default ss: var bestIndex = 0; for (var i = 1; i < available.Length; i++) { - var bestV = available[bestIndex]; - var availableV = available[i]; - if (availableV.DefaultRule > bestV.DefaultRule) + var bestSs = available[bestIndex]; + var availableSs = available[i]; + if (availableSs.DefaultRule > bestSs.DefaultRule) { bestIndex = i; } - else if (availableV.DefaultRule == bestV.DefaultRule) + else if (availableSs.DefaultRule == bestSs.DefaultRule) { - if (availableV.DefaultRule != VariantType.DefaultType.NotDefault) + if (availableSs.DefaultRule != ComponentTypeSerializationStrategy.DefaultType.NotDefault) { - BurstCompatibleErrorWithAggregate(in available, $"Type `{componentType}` (isRoot: {isRoot}) has 2 or more default variants with the same `DefaultRule` ({(int) availableV.DefaultRule})! Using the first. DefaultVariants: {DefaultVariants.Count}."); + BurstCompatibleErrorWithAggregate(componentType, in available, $"Type `{componentType.ToFixedString()}` (isRoot: {isRoot}) has 2 or more default serialization strategies with the same `DefaultRule` ({(int) availableSs.DefaultRule})! Using the first. DefaultVariants: {DefaultVariants.Count}."); } } } var finalVariant = available[bestIndex]; - if (finalVariant.DefaultRule != VariantType.DefaultType.NotDefault) + if (finalVariant.DefaultRule != ComponentTypeSerializationStrategy.DefaultType.NotDefault) return finalVariant; // We failed, so get the safest fallback: var fallback = GetSafestFallbackVariantUponError(available); - BurstCompatibleErrorWithAggregate(in available, $"Type `{componentType}` (isRoot: {isRoot}) has NO default variants! Calculating the safest fallback guess ('{fallback.ToFixedString()}'). DefaultVariants: {DefaultVariants.Count}."); + BurstCompatibleErrorWithAggregate(componentType, in available, $"Type `{componentType.ToFixedString()}` (isRoot: {isRoot}) has NO default serialization strategies! Calculating the safest fallback guess ('{fallback.ToFixedString()}'). DefaultVariants: {DefaultVariants.Count}."); return fallback; } // Find the EXACT variant by hash. foreach (var variant in available) - if (variant.Hash == variantHash) + if (variant.Hash == serializationStrategyHash) return variant; // Couldn't find any, so try to get the safest fallback: if (available.Length != 0) { var fallback = GetSafestFallbackVariantUponError(available); - BurstCompatibleErrorWithAggregate(in available, $"Failed to find variant for `{componentType}` (isRoot: {isRoot}) with hash '{variantHash}'! There are {available.Length} variants available, so calculating the safest fallback guess ('{fallback.ToFixedString()}'). DefaultVariants: {DefaultVariants.Count}."); + BurstCompatibleErrorWithAggregate(componentType, in available, $"Failed to find serialization strategy for `{componentType.ToFixedString()}` (isRoot: {isRoot}) with hash '{serializationStrategyHash}'! There are {available.Length} serialization strategies available, so calculating the safest fallback guess ('{fallback.ToFixedString()}'). DefaultVariants: {DefaultVariants.Count}."); return fallback; } } // Failed to find anything, so fallback: - BurstCompatibleErrorWithAggregate(in available, $"Unable to find variantHash '{variantHash}' for `{componentType}` (isRoot: {isRoot}) as no variants available for type! Fallback is `DontSerializeVariant`."); - return ConstructDontSerializeVariant(componentType, VariantType.DefaultType.YesViaFallbackRule); + BurstCompatibleErrorWithAggregate(componentType, in available, $"Unable to find serializationStrategyHash '{serializationStrategyHash}' for `{componentType.ToFixedString()}` (isRoot: {isRoot}) as no serialization strategies available for type! Fallback is `DontSerializeVariant`."); + return ConstructDontSerializeVariant(componentType, ComponentTypeSerializationStrategy.DefaultType.YesAsIsFallback); } /// When we are unable to find the requested variant, this method finds the best fallback. - static VariantType GetSafestFallbackVariantUponError(in NativeList available) + static ComponentTypeSerializationStrategy GetSafestFallbackVariantUponError(in NativeList available) { // Prefer to serialize all data on the ghost. Potentially wasteful, but "safest" as data will be replicated. for (var i = 0; i < available.Length; i++) { - if (available[i].IsSerialized && available[i].IsDefaultSerializer) + if (available[i].IsSerialized != 0 && available[i].IsDefaultSerializer != 0) return available[i]; } // Otherwise fallback to a serialized variant. for (var i = 0; i < available.Length; i++) { - if (available[i].IsSerialized) + if (available[i].IsSerialized != 0) return available[i]; } @@ -511,72 +482,45 @@ static VariantType GetSafestFallbackVariantUponError(in NativeList /// A list of all available variants for this `componentType`. [GenerateTestsForBurstCompatibility] [BurstCompile] - public NativeList GetAllAvailableVariantsForType(ComponentType componentType, bool isRoot) + public NativeList GetAllAvailableSerializationStrategiesForType(ComponentType componentType, bool isRoot) { - var availableVariants = new NativeList(4, Allocator.Temp); + var availableVariants = new NativeList(4, Allocator.Temp); var numCustomVariants = 0; var customVariantIndex = -1; - var metaData = GetOrCreateMetaData(componentType); - - // Code-gen: "Default Serializers". - foreach (var state in GhostComponentCollection.GetValuesForKey(componentType)) + // Code-gen: "Serialization Strategies" are generated and mapped here. + foreach (var strategyLookup in SerializationStrategiesComponentTypeMap.GetValuesForKey(componentType)) { - if (state.ComponentType == componentType) - { - var defaultType = CalculateDefaultTypeForSerializer(componentType, metaData, in state, isRoot); - var variantMetaData = GetOrCreateMetaData(state.VariantHash); - var variant = new VariantType - { - Component = componentType, - VariantTypeIndex = state.VariantTypeIndex, - Hash = state.VariantHash, - DefaultRule = defaultType, - IsSerialized = true, - IsTestVariant = variantMetaData.IsTestVariant, - IsDefaultSerializer = state.IsDefaultSerializer, - PrefabType = state.PrefabType, - Source = VariantType.VariantSource.SourceGeneratorSerializers, - }; - - AddAndCount(ref variant); - } - } + var strategy = SerializationStrategies[strategyLookup]; - // Code-gen: Empty variants are "Default Serializers" but without any GhostFields. Thus, "empty" serializers. - foreach (var variant in EmptyVariants.GetValuesForKey(componentType)) - { - if (variant.Component == componentType) - { - var copyOfVariant = variant; - copyOfVariant.IsSerialized = false; - copyOfVariant.DefaultRule |= CalculateDefaultTypeForNonSerializedType(componentType, variant.Hash, isRoot, availableVariants.Length > 0); - copyOfVariant.Source = VariantType.VariantSource.SourceGeneratorEmptyVariants; + if (strategy.IsSerialized != 0) + strategy.DefaultRule |= CalculateDefaultTypeForSerializer(componentType, isRoot, strategy.IsDefaultSerializer, strategy.IsInput, strategy.Hash, strategy.SendForChildEntities); + else + strategy.DefaultRule |= CalculateDefaultTypeForNonSerializedType(componentType, strategy.Hash, isRoot, availableVariants.Length > 0); - AddAndCount(ref copyOfVariant); - } + AddAndCount(ref strategy); } // `ClientOnlyVariant` special case: if (VariantIsUserSpecifiedDefaultRule(componentType, GhostVariantsUtility.ClientOnlyHash, isRoot)) { - var clientOnlyVariant = new VariantType + var clientOnlyVariant = new ComponentTypeSerializationStrategy { Component = componentType, - DefaultRule = VariantType.DefaultType.YesViaUserSpecifiedNamedDefault, - IsSerialized = false, - VariantTypeIndex = 1, // Hardcoded index lookup. + DefaultRule = ComponentTypeSerializationStrategy.DefaultType.YesViaUserSpecifiedNamedDefault, + SerializerIndex = -1, // Client only so non-serialized. No need to warn, as this is expected behaviour for all GhostEnabledBits, when using `ClientOnlyVariant`. + SelfIndex = -1, // Hardcoded index lookup. PrefabType = GhostPrefabType.Client, Hash = GhostVariantsUtility.ClientOnlyHash, - Source = VariantType.VariantSource.ManualClientOnlyVariant, + DisplayName = nameof(ClientOnlyVariant), }; - ThrowIfNoHash(clientOnlyVariant.Hash, $"'{WorldName}': ClientOnlyVariant for '{componentType}'"); + AddSerializationStrategy(ref clientOnlyVariant); AddAndCount(ref clientOnlyVariant); } // `DontSerializeVariant` special case: - if (!metaData.IsInput && AllVariantsAreSerialized(in availableVariants)) + if (!IsInput(availableVariants) && AllVariantsAreSerialized(in availableVariants)) { var defaultTypeForDontSerializeVariant = CalculateDefaultTypeForNonSerializedType(componentType, GhostVariantsUtility.DontSerializeHash, isRoot, availableVariants.Length > 0); var dontSerializeVariant = ConstructDontSerializeVariant(componentType, defaultTypeForDontSerializeVariant); @@ -588,7 +532,7 @@ public NativeList GetAllAvailableVariantsForType(ComponentType comp if (numCustomVariants == 1) { var customVariantFallback = availableVariants[customVariantIndex]; - customVariantFallback.DefaultRule |= VariantType.DefaultType.YesAsOnlyOneVariantBecomesDefault; + customVariantFallback.DefaultRule |= ComponentTypeSerializationStrategy.DefaultType.YesAsOnlyOneVariantBecomesDefault; availableVariants[customVariantIndex] = customVariantFallback; } @@ -596,7 +540,7 @@ public NativeList GetAllAvailableVariantsForType(ComponentType comp return availableVariants; - void AddAndCount(ref VariantType variant) + void AddAndCount(ref ComponentTypeSerializationStrategy variant) { if (IsUserCreatedVariant(variant.Hash, variant.IsDefaultSerializer)) { @@ -604,98 +548,83 @@ void AddAndCount(ref VariantType variant) customVariantIndex = availableVariants.Length; } - if (variant.IsTestVariant) + if (variant.IsTestVariant != 0) { - variant.DefaultRule |= VariantType.DefaultType.YesAsEditorDefault; + variant.DefaultRule |= ComponentTypeSerializationStrategy.DefaultType.YesAsEditorDefault; } availableVariants.Add(variant); } - static bool IsUserCreatedVariant(ulong variantTypeHash, bool isDefaultSerializer) + static bool IsUserCreatedVariant(ulong variantTypeHash, byte isDefaultSerializer) { - return !isDefaultSerializer && variantTypeHash != GhostVariantsUtility.DontSerializeHash && variantTypeHash != GhostVariantsUtility.ClientOnlyHash; + return isDefaultSerializer == 0 && variantTypeHash != GhostVariantsUtility.DontSerializeHash && variantTypeHash != GhostVariantsUtility.ClientOnlyHash; } } - VariantType ConstructDontSerializeVariant(ComponentType componentType, VariantType.DefaultType defaultType) + static bool IsInput(NativeList availableVariants) { - var dontSerializeVariant = new VariantType + foreach (var ss in availableVariants) + if(ss.IsInput != 0) + return true; + return false; + } + + ComponentTypeSerializationStrategy ConstructDontSerializeVariant(ComponentType componentType, ComponentTypeSerializationStrategy.DefaultType defaultType) + { + var dontSerializeVariant = new ComponentTypeSerializationStrategy { Component = componentType, DefaultRule = defaultType, - IsSerialized = false, - VariantTypeIndex = 0, // Hardcoded index lookup. + SerializerIndex = -1, + SelfIndex = -1, PrefabType = GhostPrefabType.All, Hash = GhostVariantsUtility.DontSerializeHash, - Source = VariantType.VariantSource.ManualDontSerializeVariant, + DisplayName = nameof(DontSerializeVariant), }; - ThrowIfNoHash(dontSerializeVariant.Hash, $"'{WorldName}': ConstructDontSerializeVariant for '{componentType}'"); + AddSerializationStrategy(ref dontSerializeVariant); return dontSerializeVariant; } - /// Fetch meta-data for any component. Used to avoid reflection. - internal CodeGenTypeMetaData GetOrCreateMetaData(ComponentType componentType) - { - var hash = GhostVariantsUtility.CalculateVariantHashForComponent(componentType); - return GetOrCreateMetaData(hash); - } - - /// Fetches the meta-data from the cache (or builds one if one does not exist). - internal CodeGenTypeMetaData GetOrCreateMetaData(ulong variantTypeHash) - { - ThrowIfNoHash(variantTypeHash, $"'{WorldName}': GetOrCreateMetaData for {variantTypeHash}"); - - if (!CodeGenTypeMetaData.TryGetValue(variantTypeHash, out var metaData)) - { - CodeGenTypeMetaData[variantTypeHash] = metaData = new CodeGenTypeMetaData - { - TypeHash = variantTypeHash, - IsInputComponent = false, - IsInputBuffer = false, - IsTestVariant = false, - HasDontSupportPrefabOverridesAttribute = false, - HasSupportsPrefabOverridesAttribute = false, - }; - } - return metaData; - } - - static bool AllVariantsAreSerialized(in NativeList availableVariants) + static bool AllVariantsAreSerialized(in NativeList availableVariants) { foreach (var x in availableVariants) { - if (!x.IsSerialized) + if (x.IsSerialized == 0) return false; } return true; } - internal static bool AnyVariantsAreSerialized(in NativeList availableVariants) + internal static bool AnyVariantsAreSerialized(in NativeList availableVariants) { foreach (var x in availableVariants) { - if (x.IsSerialized) + if (x.IsSerialized != 0) return true; } return false; } - void BurstCompatibleErrorWithAggregate(in NativeList availableVariants, FixedString4096Bytes error) + void BurstCompatibleErrorWithAggregate(ComponentType componentType, in NativeList availableVariants, FixedString4096Bytes error) { error.Append(WorldName); - error.Append((FixedString64Bytes)$", {availableVariants.Length} variants available: "); - for (var i = 0; i < availableVariants.Length; i++) + error.Append(' '); + error.Append(componentType.ToFixedString()); + if (availableVariants.IsCreated) { - var availableVariant = availableVariants[i]; - error.Append('\n'); - error.Append(i); - error.Append(':'); - error.Append(availableVariant.ToFixedString()); + error.Append((FixedString64Bytes) $", {availableVariants.Length} variants available: "); + for (var i = 0; i < availableVariants.Length; i++) + { + var availableVariant = availableVariants[i]; + error.Append('\n'); + error.Append(i); + error.Append(':'); + error.Append(availableVariant.ToFixedString()); + } } - UnityEngine.Debug.LogError(error); } @@ -708,21 +637,21 @@ void BurstCompatibleErrorWithAggregate(in NativeList availableVaria /// return true ONLY IF it's the default serializer (i.e. variantType == componentType). /// 3. Otherwise, if this is component is on a child entity, return true if it's the . /// - VariantType.DefaultType CalculateDefaultTypeForSerializer(ComponentType componentType, CodeGenTypeMetaData metaData, in GhostComponentSerializer.State state, bool isRoot) + ComponentTypeSerializationStrategy.DefaultType CalculateDefaultTypeForSerializer(ComponentType componentType, bool isRoot, byte isDefaultSerializer, byte isInput, ulong ssHash, byte sendForChildEntities) { - if (VariantIsUserSpecifiedDefaultRule(componentType, state.VariantHash, isRoot)) - return VariantType.DefaultType.YesViaUserSpecifiedNamedDefault; + if (VariantIsUserSpecifiedDefaultRule(componentType, ssHash, isRoot)) + return ComponentTypeSerializationStrategy.DefaultType.YesViaUserSpecifiedNamedDefault; // The user did NOT specify this as a default, so infer defaults from rules: - if (isRoot || metaData.IsInput) - return state.IsDefaultSerializer ? VariantType.DefaultType.YesAsIsDefaultSerializerAndIsRoot : VariantType.DefaultType.NotDefault; + if (isRoot || isInput != 0) + return isDefaultSerializer != 0 ? ComponentTypeSerializationStrategy.DefaultType.YesAsIsDefaultSerializerAndIsRoot : ComponentTypeSerializationStrategy.DefaultType.NotDefault; // Child entities default to DontSerializeVariant: // But that may have been changed via attribute: - if (state.SendForChildEntities) - return state.IsDefaultSerializer ? VariantType.DefaultType.YesAsAttributeAllowingChildSerialization : VariantType.DefaultType.NotDefault; + if (sendForChildEntities != 0) + return isDefaultSerializer != 0 ? ComponentTypeSerializationStrategy.DefaultType.YesAsAttributeAllowingChildSerialization : ComponentTypeSerializationStrategy.DefaultType.NotDefault; - return state.VariantHash == GhostVariantsUtility.DontSerializeHash ? VariantType.DefaultType.YesViaFallbackRule : VariantType.DefaultType.NotDefault; + return ssHash == GhostVariantsUtility.DontSerializeHash ? ComponentTypeSerializationStrategy.DefaultType.YesAsIsChildDefaultingToDontSerializeVariant : ComponentTypeSerializationStrategy.DefaultType.NotDefault; } /// @@ -734,11 +663,11 @@ VariantType.DefaultType CalculateDefaultTypeForSerializer(ComponentType componen /// return true ONLY IF it's the default serializer (i.e. variantType == componentType). /// 3. Otherwise, if this is component is on a child entity, return true if it's the . /// - VariantType.DefaultType CalculateDefaultTypeForNonSerializedType(ComponentType componentType, ulong variantTypeHash, bool isRoot, bool hasAnyAvailableVariants) + ComponentTypeSerializationStrategy.DefaultType CalculateDefaultTypeForNonSerializedType(ComponentType componentType, ulong variantTypeHash, bool isRoot, bool hasAnyAvailableVariants) { if (VariantIsUserSpecifiedDefaultRule(componentType, variantTypeHash, isRoot)) - return VariantType.DefaultType.YesViaUserSpecifiedNamedDefault; - return isRoot && hasAnyAvailableVariants ? VariantType.DefaultType.NotDefault : VariantType.DefaultType.YesAsIsChildDefaultingToDontSerializeVariant; + return ComponentTypeSerializationStrategy.DefaultType.YesViaUserSpecifiedNamedDefault; + return isRoot && hasAnyAvailableVariants ? ComponentTypeSerializationStrategy.DefaultType.NotDefault : ComponentTypeSerializationStrategy.DefaultType.YesAsIsChildDefaultingToDontSerializeVariant; } bool VariantIsUserSpecifiedDefaultRule(ComponentType componentType, ulong variantTypeHash, bool isRoot) @@ -766,5 +695,38 @@ public static void ThrowIfNoHash(ulong hash, FixedString512Bytes context) if (hash == 0) throw new InvalidOperationException($"Cannot add variant for context '{context}' as hash is zero! Set hashes for all variants via `GhostVariantsUtility` and ensure you've rebuilt NetCode 'Source Generators'."); } + + /// Release the allocated resources used to store the ghost serializer strategies and mappings. + public void Dispose() + { + Serializers.Dispose(); + SerializationStrategies.Dispose(); + DefaultVariants.Dispose(); + SerializationStrategiesCache.Dispose(); + SerializationStrategiesComponentTypeMap.Dispose(); + } + + /// + /// Validate that all the serialization strategies have a valid + /// and that all the have been set. + /// + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + public void Validate() + { + for (var i = 0; i < SerializationStrategies.Length; i++) + { + var serializationStrategy = SerializationStrategies[i]; + UnityEngine.Assertions.Assert.AreEqual(i, serializationStrategy.SelfIndex, "SerializationStrategies[i]"); + if (serializationStrategy.SerializerIndex >= 0) + { + UnityEngine.Assertions.Assert.IsTrue(serializationStrategy.SerializerIndex < Serializers.Length, "SerializationStrategies > Serializer Index in Range"); + UnityEngine.Assertions.Assert.AreEqual(i, Serializers[serializationStrategy.SerializerIndex].SerializationStrategyIndex, "SerializationStrategies > Serializer > SerializationStrategies backwards lookup!"); + } + } + foreach (var serializer in Serializers) + { + UnityEngine.Assertions.Assert.IsTrue(serializer.SerializationStrategyIndex >= 0 && serializer.SerializationStrategyIndex < SerializationStrategies.Length, "Serializer > SerializationStrategies Index in Range"); + } + } } } diff --git a/Runtime/Snapshot/GhostDistancePartitioningSystem.cs b/Runtime/Snapshot/GhostDistancePartitioningSystem.cs index 46f46d5..6d7d5d1 100644 --- a/Runtime/Snapshot/GhostDistancePartitioningSystem.cs +++ b/Runtime/Snapshot/GhostDistancePartitioningSystem.cs @@ -34,14 +34,22 @@ public partial struct GhostDistancePartitioningSystem : ISystem { EntityQuery m_EntityQuery; EntityTypeHandle m_EntityTypeHandle; +#if !ENABLE_TRANSFORM_V1 + ComponentTypeHandle m_Transform; +#else ComponentTypeHandle m_Translation; +#endif SharedComponentTypeHandle m_SharedPartition; [BurstCompile] struct UpdateTileIndexJob : IJobChunk { [ReadOnly] public SharedComponentTypeHandle TileTypeHandle; +#if !ENABLE_TRANSFORM_V1 + [ReadOnly] public ComponentTypeHandle TransHandle; +#else [ReadOnly] public ComponentTypeHandle TransHandle; +#endif [ReadOnly] public EntityTypeHandle EntityTypeHandle; public GhostDistanceData Config; public EntityCommandBuffer.ParallelWriter Ecb; @@ -50,20 +58,37 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE { Assert.IsFalse(useEnabledMask); var tile = chunk.GetSharedComponent(TileTypeHandle); - var translations = chunk.GetNativeArray(TransHandle); +#if !ENABLE_TRANSFORM_V1 + var transforms = chunk.GetNativeArray(ref TransHandle); +#else + var translations = chunk.GetNativeArray(ref TransHandle); +#endif var entities = chunk.GetNativeArray(EntityTypeHandle); +#if !ENABLE_TRANSFORM_V1 + for (var index = 0; index < transforms.Length; index++) + { + var transform = transforms[index]; + var origTilePos = tile.Index * Config.TileSize + Config.TileCenter; + if (math.all(transform.Position >= origTilePos - Config.TileBorderWidth) && + math.all(transform.Position <= origTilePos + Config.TileSize + Config.TileBorderWidth)) +#else for (var index = 0; index < translations.Length; index++) { var translation = translations[index]; var origTilePos = tile.Index * Config.TileSize + Config.TileCenter; if (math.all(translation.Value >= origTilePos - Config.TileBorderWidth) && math.all(translation.Value <= origTilePos + Config.TileSize + Config.TileBorderWidth)) +#endif { continue; } +#if !ENABLE_TRANSFORM_V1 + var tileIndex = ((int3)transform.Position - Config.TileCenter) / Config.TileSize; +#else var tileIndex = ((int3)translation.Value - Config.TileCenter) / Config.TileSize; +#endif if (math.all(tile.Index == tileIndex)) { continue; @@ -81,13 +106,21 @@ partial struct AddSharedDistancePartitionJob : IJobEntity public GhostDistanceData Config; public EntityCommandBuffer.ParallelWriter ConcurrentCommandBuffer; - void Execute(Entity ent, [EntityInQueryIndex]int entityInQueryIndex, in Translation trans, in GhostComponent ghost) +#if !ENABLE_TRANSFORM_V1 + void Execute(Entity ent, [EntityIndexInQuery]int entityIndexInQuery, in LocalTransform trans, in GhostComponent ghost) + { + var tileIndex = ((int3) trans.Position - Config.TileCenter) / Config.TileSize; + ConcurrentCommandBuffer.AddSharedComponent(entityIndexInQuery, ent, new GhostDistancePartitionShared{Index = tileIndex}); + } + } +#else + void Execute(Entity ent, [EntityIndexInQuery]int entityIndexInQuery, in Translation trans, in GhostComponent ghost) { var tileIndex = ((int3) trans.Value - Config.TileCenter) / Config.TileSize; - ConcurrentCommandBuffer.AddSharedComponent(entityInQueryIndex, ent, new GhostDistancePartitionShared{Index = tileIndex}); + ConcurrentCommandBuffer.AddSharedComponent(entityIndexInQuery, ent, new GhostDistancePartitionShared{Index = tileIndex}); } } - +#endif [BurstCompile] public void OnDestroy(ref SystemState state) { @@ -97,7 +130,21 @@ public void OnDestroy(ref SystemState state) public void OnUpdate(ref SystemState state) { var config = SystemAPI.GetSingleton(); - +#if ENABLE_UNITY_COLLECTIONS_CHECKS || NETCODE_DEBUG + //Validate that the DistanceData contains valid ranges and values + if (config.TileSize.Equals(int3.zero)) + { + var netDebug = SystemAPI.GetSingleton(); + netDebug.LogError("GhostDistanceData.TileSize must always be different than int3.zero. You must specify a non zero tile size for at least one of the axis."); + return; + } + if (config.TileSize.x < 0 || config.TileSize.y < 0 || config.TileSize.z < 0) + { + var netDebug = SystemAPI.GetSingleton(); + netDebug.LogError($"Invalid GhostDistanceData.TileSize ({config.TileSize}) set for GhostDistanceData singleton.\nThe tile size for each individual axis must be a value greater than or equals zero"); + return; + } +#endif var barrier = SystemAPI.GetSingleton(); var sharedPartitionHandle = new AddSharedDistancePartitionJob { @@ -106,15 +153,24 @@ public void OnUpdate(ref SystemState state) }.Schedule(state.Dependency); m_EntityTypeHandle.Update(ref state); +#if !ENABLE_TRANSFORM_V1 + m_Transform.Update(ref state); +#else m_Translation.Update(ref state); +#endif m_SharedPartition.Update(ref state); + state.Dependency = new UpdateTileIndexJob { Config = config, Ecb = barrier.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter(), EntityTypeHandle = m_EntityTypeHandle, TileTypeHandle = m_SharedPartition, +#if !ENABLE_TRANSFORM_V1 + TransHandle = m_Transform, +#else TransHandle = m_Translation, +#endif }.ScheduleParallel(m_EntityQuery, sharedPartitionHandle); } @@ -122,12 +178,20 @@ public void OnUpdate(ref SystemState state) public void OnCreate(ref SystemState state) { m_EntityTypeHandle = state.GetEntityTypeHandle(); +#if !ENABLE_TRANSFORM_V1 + m_Transform = state.GetComponentTypeHandle(true); +#else m_Translation = state.GetComponentTypeHandle(true); +#endif m_SharedPartition = state.GetSharedComponentTypeHandle(); state.RequireForUpdate(); state.RequireForUpdate(); var builder = new EntityQueryBuilder(Allocator.Temp) +#if !ENABLE_TRANSFORM_V1 + .WithAll(); +#else .WithAll(); +#endif m_EntityQuery = state.WorldUnmanaged.EntityManager.CreateEntityQuery(builder); } } diff --git a/Runtime/Snapshot/GhostPreSerializer.cs b/Runtime/Snapshot/GhostPreSerializer.cs index 51c5432..7416cfb 100644 --- a/Runtime/Snapshot/GhostPreSerializer.cs +++ b/Runtime/Snapshot/GhostPreSerializer.cs @@ -147,14 +147,14 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE Assert.IsFalse(useEnabledMask); var GhostTypeCollection = GhostTypeCollectionFromEntity[GhostCollectionSingleton]; DynamicComponentTypeHandle* ghostChunkComponentTypesPtr = List.GetData(); - var ghosts = chunk.GetNativeArray(ghostComponentType); + var ghosts = chunk.GetNativeArray(ref ghostComponentType); // Find the ghost type for this chunk var ghostType = ghosts[0].ghostType; // Pre spawned ghosts might not have a proper ghost type index yet, we calculate it here for pre spawns if (ghostType < 0) { var GhostCollection = GhostCollectionFromEntity[GhostCollectionSingleton]; - var ghostTypeComponent = chunk.GetNativeArray(ghostTypeComponentType)[0]; + var ghostTypeComponent = chunk.GetNativeArray(ref ghostTypeComponentType)[0]; for (ghostType = 0; ghostType < GhostCollection.Length; ++ghostType) { if (GhostCollection[ghostType].GhostType == ghostTypeComponent) diff --git a/Runtime/Snapshot/GhostPredictionDebugSystem.cs b/Runtime/Snapshot/GhostPredictionDebugSystem.cs index 2363a6b..19a985d 100644 --- a/Runtime/Snapshot/GhostPredictionDebugSystem.cs +++ b/Runtime/Snapshot/GhostPredictionDebugSystem.cs @@ -96,7 +96,11 @@ public void OnUpdate(ref SystemState state) childEntityLookup = state.GetEntityStorageInfoLookup(), linkedEntityGroupType = m_LinkedEntityGroupHandle, tick = networkTime.ServerTick, +#if !ENABLE_TRANSFORM_V1 + transformType = ComponentType.ReadWrite(), +#else translationType = ComponentType.ReadWrite(), +#endif predictionErrors = m_PredictionErrors.AsArray(), numPredictionErrors = predictionErrorCount @@ -171,7 +175,11 @@ struct PredictionDebugJob : IJobChunk public NetworkTick tick; // FIXME: placeholder to show the idea behind prediction smoothing +#if !ENABLE_TRANSFORM_V1 + public ComponentType transformType; +#else public ComponentType translationType; +#endif const GhostComponentSerializer.SendMask requiredSendMask = GhostComponentSerializer.SendMask.Predicted; @@ -196,7 +204,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var GhostComponentIndex = GhostComponentIndexFromEntity[GhostCollectionSingleton]; var GhostComponentCollection = GhostComponentCollectionFromEntity[GhostCollectionSingleton]; - var ghostComponents = chunk.GetNativeArray(ghostType); + var ghostComponents = chunk.GetNativeArray(ref ghostType); int ghostTypeId = ghostComponents.GetFirstGhostTypeId(); if (ghostTypeId < 0) return; @@ -208,7 +216,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE Entity* backupEntities = PredictionBackupState.GetEntities(state); var entities = chunk.GetNativeArray(entityType); - var predictedGhostComponents = chunk.GetNativeArray(predictedGhostType); + var predictedGhostComponents = chunk.GetNativeArray(ref predictedGhostType); byte* dataPtr = PredictionBackupState.GetData(state); int numBaseComponents = typeData.NumComponents - typeData.NumChildComponents; @@ -226,11 +234,11 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var compSize = GhostComponentCollection[serializerIdx].ComponentType.IsBuffer ? GhostSystemConstants.DynamicBufferComponentSnapshotSize : GhostComponentCollection[serializerIdx].ComponentSize; - if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { - var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); for (int ent = 0; ent < entities.Length; ++ent) { // If this entity did not predict anything there was no rollback and no need to debug it @@ -257,7 +265,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE } if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[baseOffset + comp].ComponentIndex; @@ -283,11 +291,11 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE continue; var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[entityIdx].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { - var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); int errorIndex = GhostComponentIndex[baseOffset + comp].PredictionErrorBaseIndex; float* errorsPtr = ((float*)predictionErrors.GetUnsafePtr()) + errorIndex + ThreadIndex * numPredictionErrors; diff --git a/Runtime/Snapshot/GhostPredictionHistorySystem.cs b/Runtime/Snapshot/GhostPredictionHistorySystem.cs index 458f755..9041678 100644 --- a/Runtime/Snapshot/GhostPredictionHistorySystem.cs +++ b/Runtime/Snapshot/GhostPredictionHistorySystem.cs @@ -272,7 +272,7 @@ public void OnUpdate(ref SystemState state) state.Dependency = cleanupJob.Schedule(state.Dependency); } - //[BurstCompile] // TODO: Re-enable once Burst issue BUR-1971 is fixed. + [BurstCompile] struct CleanupPredictionStateJob : IJob { public NativeParallelHashMap predictionState; @@ -313,7 +313,7 @@ public void Execute() } } - //[BurstCompile] // TODO: Re-enable once Burst issue BUR-1971 is fixed. + [BurstCompile] struct PredictionBackupJob : IJobChunk { public DynamicTypeList DynamicTypeList; @@ -360,7 +360,7 @@ struct PredictionBackupJob : IJobChunk if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) continue; - if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { var bufferData = chunk.GetUntypedBufferAccessor(ref ghostChunkComponentTypesPtr[compIdx]); for (int i = 0; i < bufferData.Length; ++i) @@ -373,7 +373,7 @@ struct PredictionBackupJob : IJobChunk if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[baseOffset + comp].ComponentIndex; @@ -392,7 +392,7 @@ struct PredictionBackupJob : IJobChunk { var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[baseOffset + comp].EntityIndex].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { var bufferData = childChunk.Chunk.GetUntypedBufferAccessor(ref ghostChunkComponentTypesPtr[compIdx]); bufferTotalSize += bufferData.GetBufferCapacity(childChunk.IndexInChunk) * GhostComponentCollection[serializerIdx].ComponentSize; @@ -417,12 +417,12 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var GhostComponentCollection = GhostComponentCollectionFromEntity[GhostCollectionSingleton]; var GhostPrefabCollection = GhostPrefabCollectionFromEntity[GhostCollectionSingleton]; - var ghostComponents = chunk.GetNativeArray(ghostComponentType); - var ghostTypes = chunk.GetNativeArray(ghostType); + var ghostComponents = chunk.GetNativeArray(ref ghostComponentType); + var ghostTypes = chunk.GetNativeArray(ref ghostType); int ghostTypeId = ghostComponents.GetFirstGhostTypeId(); if (ghostTypeId < 0) { - if(!chunk.Has(prespawnIndexType)) + if(!chunk.Has(ref prespawnIndexType)) return; //Prespawn chunk that hasn't been received/processed yet. Since it is predicted we still @@ -473,7 +473,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE GhostComponentCollection[serializerIdx].ComponentSize, chunk.Capacity); else dataSize += PredictionBackupState.GetDataSize(GhostSystemConstants.DynamicBufferComponentSnapshotSize, chunk.Capacity); - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) ++enabledBits; } @@ -533,7 +533,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE ? GhostSystemConstants.DynamicBufferComponentSnapshotSize : GhostComponentCollection[serializerIdx].ComponentSize; - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var handle = ghostChunkComponentTypesPtr[compIdx]; var bitArray = chunk.GetEnableableBits(ref handle); @@ -541,13 +541,13 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE enabledBitPtr = PredictionBackupState.GetNextEnabledBits(enabledBitPtr, chunk.Capacity); } - if (!chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (!chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { UnsafeUtility.MemClear(dataPtr, chunk.Count * compSize); } else if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { - var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); UnsafeUtility.MemCpy(dataPtr, compData, chunk.Count * compSize); } else @@ -574,7 +574,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE } if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[baseOffset + comp].ComponentIndex; @@ -586,7 +586,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE if ((GhostComponentIndex[baseOffset + comp].SendMask&requiredSendMask) == 0) continue; - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent) { @@ -616,9 +616,9 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE { var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[baseOffset + comp].EntityIndex].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { - var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); UnsafeUtility.MemCpy(tempDataPtr, compData + childChunk.IndexInChunk * compSize, compSize); } else @@ -634,7 +634,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE { var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[baseOffset + comp].EntityIndex].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { var bufferData = childChunk.Chunk.GetUntypedBufferAccessor(ref ghostChunkComponentTypesPtr[compIdx]); //Retrieve an copy each buffer data. Set size and offset in the backup buffer in the component backup diff --git a/Runtime/Snapshot/GhostPredictionSmoothingSystem.cs b/Runtime/Snapshot/GhostPredictionSmoothingSystem.cs index 0451353..24a2e4e 100644 --- a/Runtime/Snapshot/GhostPredictionSmoothingSystem.cs +++ b/Runtime/Snapshot/GhostPredictionSmoothingSystem.cs @@ -297,7 +297,7 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo var GhostComponentIndex = GhostComponentIndexFromEntity[GhostCollectionSingleton]; var GhostComponentCollection = GhostComponentCollectionFromEntity[GhostCollectionSingleton]; - var ghostComponents = chunk.GetNativeArray(ghostType); + var ghostComponents = chunk.GetNativeArray(ref ghostType); int ghostTypeId = ghostComponents.GetFirstGhostTypeId(); if (ghostTypeId < 0) @@ -313,7 +313,7 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo Entity* backupEntities = PredictionBackupState.GetEntities(state); var entities = chunk.GetNativeArray(entityType); - var predictedGhostComponents = chunk.GetNativeArray(predictedGhostType); + var predictedGhostComponents = chunk.GetNativeArray(ref predictedGhostType); int numBaseComponents = typeData.NumComponents - typeData.NumChildComponents; int baseOffset = typeData.FirstComponent; @@ -362,7 +362,7 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo foreach (var action in actions) { - if (chunk.Has(ghostChunkComponentTypesPtr[action.compIndex])) + if (chunk.Has(ref ghostChunkComponentTypesPtr[action.compIndex])) { for (int ent = 0; ent < entities.Length; ++ent) { @@ -373,12 +373,12 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo if (entities[ent] != backupEntities[ent]) continue; - var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[action.compIndex], action.compSize).GetUnsafePtr(); + var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[action.compIndex], action.compSize).GetUnsafePtr(); void* usrDataPtr = null; - if (action.userTypeId >= 0 && chunk.Has(userTypes[action.userTypeId])) + if (action.userTypeId >= 0 && chunk.Has(ref userTypes[action.userTypeId])) { - var usrData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(userTypes[action.userTypeId], action.userTypeSize).GetUnsafeReadOnlyPtr(); + var usrData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref userTypes[action.userTypeId], action.userTypeSize).GetUnsafeReadOnlyPtr(); usrDataPtr = usrData + action.userTypeSize * ent; } @@ -389,7 +389,7 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo } } - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); foreach (var action in childActions) { for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent) @@ -402,14 +402,14 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[action.entityIndex].Value; if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && - childChunk.Chunk.Has(ghostChunkComponentTypesPtr[action.compIndex])) + childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[action.compIndex])) { - var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[action.compIndex], action.compSize).GetUnsafePtr(); + var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[action.compIndex], action.compSize).GetUnsafePtr(); void* usrDataPtr = null; - if (action.userTypeId >= 0 && chunk.Has(userTypes[action.userTypeId])) + if (action.userTypeId >= 0 && chunk.Has(ref userTypes[action.userTypeId])) { - var usrData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(userTypes[action.userTypeId], action.userTypeSize).GetUnsafeReadOnlyPtr(); + var usrData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref userTypes[action.userTypeId], action.userTypeSize).GetUnsafeReadOnlyPtr(); usrDataPtr = usrData + action.userTypeSize * ent; } diff --git a/Runtime/Snapshot/GhostPredictionSwitchingQueues.cs b/Runtime/Snapshot/GhostPredictionSwitchingQueues.cs index e3060dc..4879c56 100644 --- a/Runtime/Snapshot/GhostPredictionSwitchingQueues.cs +++ b/Runtime/Snapshot/GhostPredictionSwitchingQueues.cs @@ -220,8 +220,8 @@ static unsafe bool AddRemoveComponents(EntityManager entityManager, ref GhostUpd } else { - byte* src = (byte*)srcInfo.Chunk.GetDynamicComponentDataArrayReinterpret(typeHandle, sizeInChunk).GetUnsafeReadOnlyPtr(); - byte* dst = (byte*)dstInfo.Chunk.GetDynamicComponentDataArrayReinterpret(typeHandle, sizeInChunk).GetUnsafePtr(); + byte* src = (byte*)srcInfo.Chunk.GetDynamicComponentDataArrayReinterpret(ref typeHandle, sizeInChunk).GetUnsafeReadOnlyPtr(); + byte* dst = (byte*)dstInfo.Chunk.GetDynamicComponentDataArrayReinterpret(ref typeHandle, sizeInChunk).GetUnsafePtr(); UnsafeUtility.MemCpy(dst + dstInfo.IndexInChunk*sizeInChunk, src + srcInfo.IndexInChunk*sizeInChunk, sizeInChunk); } } @@ -233,9 +233,26 @@ static unsafe bool AddRemoveComponents(EntityManager entityManager, ref GhostUpd } if (duration > 0 && entityManager.HasComponent(entity) && +#if !ENABLE_TRANSFORM_V1 + entityManager.HasComponent(entity)) +#else entityManager.HasComponent(entity) && entityManager.HasComponent(entity)) +#endif { +#if !ENABLE_TRANSFORM_V1 + entityManager.AddComponent(entity, new ComponentTypeSet(ComponentType.ReadWrite(), + ComponentType.ReadWrite())); + var localTransform = entityManager.GetComponentData(entity); + entityManager.SetComponentData(entity, new SwitchPredictionSmoothing + { + InitialPosition = localTransform.Position, + InitialRotation = localTransform.Rotation, + CurrentFactor = 0, + Duration = duration, + SkipVersion = ghostUpdateVersion.LastSystemVersion + }); +#else entityManager.AddComponentData(entity, new SwitchPredictionSmoothing { InitialPosition = entityManager.GetComponentData(entity).Value, @@ -244,6 +261,7 @@ static unsafe bool AddRemoveComponents(EntityManager entityManager, ref GhostUpd Duration = duration, SkipVersion = ghostUpdateVersion.LastSystemVersion }); +#endif } return true; } diff --git a/Runtime/Snapshot/GhostPredictionSystemGroup.cs b/Runtime/Snapshot/GhostPredictionSystemGroup.cs index dbbf169..8ce4f2a 100644 --- a/Runtime/Snapshot/GhostPredictionSystemGroup.cs +++ b/Runtime/Snapshot/GhostPredictionSystemGroup.cs @@ -4,8 +4,6 @@ using Unity.Collections; using Unity.Core; using Unity.Entities; -using Unity.Jobs; -using Unity.Jobs.LowLevel.Unsafe; using Unity.Mathematics; using Unity.Burst; using Unity.Burst.Intrinsics; @@ -58,33 +56,33 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE { Assert.IsFalse(useEnabledMask); - var predicted = chunk.GetNativeArray(predictedHandle); + var predicted = chunk.GetNativeArray(ref predictedHandle); - if (chunk.Has(linkedEntityGroupHandle)) + if (chunk.Has(ref linkedEntityGroupHandle)) { - var linkedEntityGroupArray = chunk.GetBufferAccessor(linkedEntityGroupHandle); + var linkedEntityGroupArray = chunk.GetBufferAccessor(ref linkedEntityGroupHandle); - for(int i = 0, chunkEntityCount = chunk.ChunkEntityCount; i < chunkEntityCount; i++) + for(int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) { var shouldPredict = predicted[i].ShouldPredict(tick); - if (chunk.IsComponentEnabled(simulateHandle, i) != shouldPredict) + if (chunk.IsComponentEnabled(ref simulateHandle, i) != shouldPredict) { - chunk.SetComponentEnabled(simulateHandle, i, shouldPredict); + chunk.SetComponentEnabled(ref simulateHandle, i, shouldPredict); var linkedEntityGroup = linkedEntityGroupArray[i]; for (int child = 1; child < linkedEntityGroup.Length; ++child) { var storageInfo = storageInfoFromEntity[linkedEntityGroup[child].Value]; - if (storageInfo.Chunk.Has(ghostChildEntityHandle) && storageInfo.Chunk.Has(simulateHandle)) - storageInfo.Chunk.SetComponentEnabled(simulateHandle, storageInfo.IndexInChunk, shouldPredict); + if (storageInfo.Chunk.Has(ref ghostChildEntityHandle) && storageInfo.Chunk.Has(ref simulateHandle)) + storageInfo.Chunk.SetComponentEnabled(ref simulateHandle, storageInfo.IndexInChunk, shouldPredict); } } } } else { - for(int i = 0, chunkEntityCount = chunk.ChunkEntityCount; i < chunkEntityCount; i++) + for(int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) { - chunk.SetComponentEnabled(simulateHandle, i, predicted[i].ShouldPredict(tick)); + chunk.SetComponentEnabled(ref simulateHandle, i, predicted[i].ShouldPredict(tick)); } } } @@ -164,6 +162,7 @@ unsafe class NetcodeClientPredictionRateManager : IRateManager private EntityQuery m_GhostChildQuery; private NetworkTick m_LastFullPredictionTick; + readonly PredictedFixedStepSimulationSystemGroup m_PredictedFixedStepSimulationSystemGroup; private int m_TickIdx; private NetworkTick m_TargetTick; @@ -204,6 +203,8 @@ internal NetcodeClientPredictionRateManager(ComponentSystemGroup group) m_AppliedPredictedTicksQuery = group.World.EntityManager.CreateEntityQuery(ComponentType.ReadWrite()); m_UniqueInputTicksQuery = group.World.EntityManager.CreateEntityQuery(ComponentType.ReadWrite()); + m_PredictedFixedStepSimulationSystemGroup = group.World.GetExistingSystemManaged(); + var builder = new EntityQueryDesc { All = new[]{ComponentType.ReadWrite(), ComponentType.ReadOnly()}, @@ -251,6 +252,20 @@ public bool ShouldGroupUpdate(ComponentSystemGroup group) m_ElapsedTime -= m_FixedTimeStep * networkTime.ServerTickFraction; } + if (m_PredictedFixedStepSimulationSystemGroup != null) + { + var simulationTickRate = clientServerTickRate.SimulationTickRate; + var timestep = m_PredictedFixedStepSimulationSystemGroup.RateManager.Timestep; + var timestepFPS = (int)math.ceil(1f / timestep); + if (timestepFPS % simulationTickRate != 0) + { + m_PredictedFixedStepSimulationSystemGroup.RateManager.Timestep = 1f / simulationTickRate; + UnityEngine.Debug.LogWarning($"1 / {nameof(PredictedFixedStepSimulationSystemGroup)}.{nameof(ComponentSystemGroup.RateManager)}.{nameof(IRateManager.Timestep)}(ms): {timestepFPS}(FPS) " + + $"must be an integer multiple of {nameof(ClientServerTickRate)}.{nameof(ClientServerTickRate.SimulationTickRate)}:{simulationTickRate}(FPS).\n" + + $"Timestep will default to 1 / SimulationTickRate: {m_PredictedFixedStepSimulationSystemGroup.RateManager.Timestep} to fix this issue for now."); + } + } + // We must simulate the last full tick since the history backup is applied there appliedPredictedTicks.TryAdd(m_TargetTick, m_TargetTick); // We must simulate at the tick we used as last full tick last time since smoothing and error reporting is happening there diff --git a/Runtime/Snapshot/GhostPrefabCreation.cs b/Runtime/Snapshot/GhostPrefabCreation.cs index 4c08511..ad28021 100644 --- a/Runtime/Snapshot/GhostPrefabCreation.cs +++ b/Runtime/Snapshot/GhostPrefabCreation.cs @@ -40,7 +40,7 @@ public enum GhostModeMask /// Interpolated Ghosts are lightweight, as they perform no simulation on the client. /// Instead, their values are interpolated (via rules) from the latest few processed snapshots. /// From a timeline POV: Interpolated ghosts are behind the server. - /// /// + /// Interpolated = 1, /// /// Predicted Ghosts are predicted by the clients. I.e. Their component is enabled during the @@ -827,7 +827,7 @@ public static void CollectAllComponents(EntityManager entityManager, NativeArray if (hasOverrides && (compOverride.OverrideType & ComponentOverrideType.Variant) != 0) variant = compOverride.Variant; - var variantType = collectionData.GetCurrentVariantTypeForComponentCached(allComponents[i], variant, true); + var variantType = collectionData.GetCurrentSerializationStrategyForComponentCached(allComponents[i], variant, true); prefabTypes[i] = variantType.PrefabType; sendMasksOverride[i] = -1; variants[i] = variantType.Hash; diff --git a/Runtime/Snapshot/GhostReceiveSystem.cs b/Runtime/Snapshot/GhostReceiveSystem.cs index 42a1463..57804b1 100644 --- a/Runtime/Snapshot/GhostReceiveSystem.cs +++ b/Runtime/Snapshot/GhostReceiveSystem.cs @@ -110,16 +110,16 @@ public struct GhostDeserializerState [BurstCompile] public unsafe partial struct GhostReceiveSystem : ISystem { - private EntityQuery m_PlayerGroup; - private EntityQuery m_GhostCleanupGroup; - private EntityQuery m_SubSceneGroup; + EntityQuery m_ConnectionsQuery; + EntityQuery m_GhostCleanupQuery; + EntityQuery m_SubSceneQuery; - private NativeParallelHashMap m_GhostEntityMap; - private NativeParallelHashMap m_SpawnedGhostEntityMap; - private NativeList m_TempDynamicData; + NativeParallelHashMap m_GhostEntityMap; + NativeParallelHashMap m_SpawnedGhostEntityMap; + NativeList m_TempDynamicData; - private NativeArray m_GhostCompletionCount; - private StreamCompressionModel m_CompressionModel; + NativeArray m_GhostCompletionCount; + StreamCompressionModel m_CompressionModel; EntityTypeHandle m_EntityTypeHandle; ComponentLookup m_SnapshotDataFromEntity; @@ -144,7 +144,7 @@ public unsafe partial struct GhostReceiveSystem : ISystem BufferLookup m_PrespawnBaselineBufferFromEntity; #if NETCODE_DEBUG - private NetDebugPacket m_NetDebugPacket; + NetDebugPacket m_NetDebugPacket; #endif // This cannot be burst compiled due to NetDebugInterop.Initialize @@ -175,16 +175,16 @@ public void OnCreate(ref SystemState state) var builder = new EntityQueryBuilder(Allocator.Temp) .WithAll(); - m_PlayerGroup = state.GetEntityQuery(builder); + m_ConnectionsQuery = state.GetEntityQuery(builder); builder.Reset(); builder.WithAll() .WithNone(); - m_GhostCleanupGroup = state.GetEntityQuery(builder); + m_GhostCleanupQuery = state.GetEntityQuery(builder); builder.Reset(); builder.WithAll(); - m_SubSceneGroup = state.EntityManager.CreateEntityQuery(builder); + m_SubSceneQuery = state.EntityManager.CreateEntityQuery(builder); m_CompressionModel = StreamCompressionModel.Default; @@ -284,7 +284,7 @@ struct ReadStreamJob : IJob [NativeDisableContainerSafetyRestriction] private DynamicBuffer m_GhostTypeCollection; [NativeDisableContainerSafetyRestriction] private DynamicBuffer m_GhostComponentIndex; - public NativeList Players; + public NativeList Connections; public BufferLookup SnapshotFromEntity; public BufferLookup SnapshotDataBufferFromEntity; public BufferLookup SnapshotDynamicDataFromEntity; @@ -316,14 +316,14 @@ struct ReadStreamJob : IJob [ReadOnly] public ComponentLookup PrefabNamesFromEntity; [ReadOnly] public ComponentLookup EnableLoggingFromEntity; public FixedString128Bytes TimestampAndTick; - private byte m_EnablePacketLogging; + byte m_EnablePacketLogging; #endif public void Execute() { #if NETCODE_DEBUG FixedString512Bytes debugLog = TimestampAndTick; - m_EnablePacketLogging = EnableLoggingFromEntity.HasComponent(Players[0]) ? (byte)1u : (byte)0u; + m_EnablePacketLogging = EnableLoggingFromEntity.HasComponent(Connections[0]) ? (byte)1u : (byte)0u; if ((m_EnablePacketLogging == 1) && !NetDebugPacket.IsCreated) { NetDebug.LogError("GhostReceiveSystem: Packet logger has not been set. Aborting."); @@ -341,8 +341,8 @@ public void Execute() m_GhostComponentIndex = GhostComponentIndexFromEntity[GhostCollectionSingleton]; // FIXME: should handle any number of connections with individual ghost mappings for each - CheckPlayerIsValid(); - var snapshot = SnapshotFromEntity[Players[0]]; + CheckConnectionCountIsValid(); + var snapshot = SnapshotFromEntity[Connections[0]]; if (snapshot.Length == 0) return; @@ -361,7 +361,7 @@ public void Execute() debugLog.Append(FixedString.Format(" ServerTick:{0}\n", serverTick.ToFixedString())); #endif - var ack = SnapshotAckFromEntity[Players[0]]; + var ack = SnapshotAckFromEntity[Connections[0]]; if (ack.LastReceivedSnapshotByLocal.IsValid && !serverTick.IsNewerThan(ack.LastReceivedSnapshotByLocal)) return; @@ -426,12 +426,12 @@ public void Execute() } #endif NetDebug.LogError(FixedString.Format("GhostReceiveSystem ghost list item {0} was modified (Hash {1} -> {2})", firstPrefab + i, ghostCollection[firstPrefab + i].Hash, hash)); - CommandBuffer.AddComponent(Players[0], new NetworkStreamRequestDisconnect{Reason = NetworkStreamDisconnectReason.BadProtocolVersion}); + CommandBuffer.AddComponent(Connections[0], new NetworkStreamRequestDisconnect{Reason = NetworkStreamDisconnectReason.BadProtocolVersion}); return; } } } - SnapshotAckFromEntity[Players[0]] = ack; + SnapshotAckFromEntity[Connections[0]] = ack; if (IsThinClient == 1) return; @@ -525,7 +525,7 @@ public void Execute() // Desync - reset received snapshots ack.ReceivedSnapshotByLocalMask = 0; ack.LastReceivedSnapshotByLocal = NetworkTick.Invalid; - SnapshotAckFromEntity[Players[0]] = ack; + SnapshotAckFromEntity[Connections[0]] = ack; } } struct DeserializeData @@ -546,7 +546,7 @@ struct DeserializeData #endif } - private bool DeserializeEntity(NetworkTick serverTick, ref DataStreamReader dataStream, ref DeserializeData data) + bool DeserializeEntity(NetworkTick serverTick, ref DataStreamReader dataStream, ref DeserializeData data) { #if NETCODE_DEBUG FixedString512Bytes debugLog = default; @@ -1170,12 +1170,12 @@ private bool DeserializeEntity(NetworkTick serverTick, ref DataStreamReader data #if NETCODE_DEBUG var ghostCollection = GhostCollectionFromEntity[GhostCollectionSingleton]; var prefabName = FixedString.Format("{0}({1})", PrefabNamesFromEntity[ghostCollection[(int)data.TargetArch].GhostPrefab].Name, data.TargetArch); - NetDebug.LogError(FixedString.Format("Failed to decode ghost {0} of type {1}, got {2} bits expected {3} bits", ghostId, prefabName, bitsRead, ghostDataSizeInBits)); + NetDebug.LogError(FixedString.Format("Failed to decode ghost {0} of type {1}, got {2} bits, expected {3} bits", ghostId, prefabName, bitsRead, ghostDataSizeInBits)); if (m_EnablePacketLogging == 1) - NetDebugPacket.Log(FixedString.Format("ERROR: Failed to decode ghost {0} of type {1}, got {2} bits expected {3} bits", ghostId, prefabName, bitsRead, ghostDataSizeInBits)); + NetDebugPacket.Log(FixedString.Format("ERROR: Failed to decode ghost {0} of type {1}, got {2} bits, expected {3} bits", ghostId, prefabName, bitsRead, ghostDataSizeInBits)); #else - NetDebug.LogError(FixedString.Format("Failed to decode ghost {0} of type {1}, got {2} bits expected {3} bits", ghostId, data.TargetArch, bitsRead, ghostDataSizeInBits)); + NetDebug.LogError(FixedString.Format("Failed to decode ghost {0} of type {1}, got {2} bits, expected {3} bits", ghostId, data.TargetArch, bitsRead, ghostDataSizeInBits)); #endif return false; } @@ -1202,14 +1202,14 @@ private bool DeserializeEntity(NetworkTick serverTick, ref DataStreamReader data } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - private void CheckPrespawnBaselineIsPresent(Entity gent, int ghostId) + void CheckPrespawnBaselineIsPresent(Entity gent, int ghostId) { if (!PrespawnBaselineBufferFromEntity.HasBuffer(gent)) throw new InvalidOperationException($"Prespawn baseline for ghost with id {ghostId} not present"); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - private static void CheckPrespawnBaselinePtrsAreValid(DeserializeData data, byte* baselineData, int ghostId, + static void CheckPrespawnBaselinePtrsAreValid(DeserializeData data, byte* baselineData, int ghostId, byte* baselineDynamicDataPtr) { if (baselineData == null) @@ -1221,40 +1221,44 @@ private void CheckPrespawnBaselineIsPresent(Entity gent, int ghostId) } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - private static void CheckDynamicSnapshotBufferOverflow(uint dynamicBufferOffset, uint maskSize, uint dynamicDataSize, + static void CheckDynamicSnapshotBufferOverflow(uint dynamicBufferOffset, uint maskSize, uint dynamicDataSize, uint snapshotDynamicDataCapacity) { if ((dynamicBufferOffset + maskSize + dynamicDataSize) > snapshotDynamicDataCapacity) - throw new InvalidOperationException("DynamicData Snapshot buffer overflow during deserialize"); + throw new InvalidOperationException($"DynamicData Snapshot buffer overflow during deserialize! dynamicBufferOffset({dynamicBufferOffset}) + maskSize({maskSize}) + dynamicDataSize({dynamicDataSize}) must be <= snapshotDynamicDataCapacity({snapshotDynamicDataCapacity})!"); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - private void CheckSnaphostBufferOverflow(int maskOffset, int maskBits, int totalMaskBits, + void CheckSnaphostBufferOverflow(int maskOffset, int maskBits, int totalMaskBits, int snapshotOffset, int snapshotSize, int bufferSize) { - if (maskOffset + maskBits > totalMaskBits || snapshotOffset + GhostComponentSerializer.SnapshotSizeAligned(snapshotSize) > bufferSize) - throw new InvalidOperationException("Snapshot buffer overflow during deserialize"); + if (maskOffset + maskBits > totalMaskBits) + throw new InvalidOperationException($"Snapshot buffer overflow during deserialize: maskOffset({maskOffset}) + maskBits({maskBits}) must be <= totalMaskBits({totalMaskBits})!"); + var snapshotSizeAligned = GhostComponentSerializer.SnapshotSizeAligned(snapshotSize); + if (snapshotOffset + snapshotSizeAligned > bufferSize) + throw new InvalidOperationException($"Snapshot buffer overflow during deserialize: snapshotOffset({snapshotOffset}) + snapshotSizeAligned({snapshotSizeAligned}) must be <= bufferSize({bufferSize})!"); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - private static void CheckOffsetLessThanSnapshotBufferSize(int snapshotOffset, int snapshotSize, int bufferSize) + static void CheckOffsetLessThanSnapshotBufferSize(int snapshotOffset, int snapshotSize, int bufferSize) { - if (snapshotOffset + GhostComponentSerializer.SnapshotSizeAligned(snapshotSize) > bufferSize) - throw new InvalidOperationException("Snapshot buffer overflow during predict"); + var snapshotSizeAligned = GhostComponentSerializer.SnapshotSizeAligned(snapshotSize); + if (snapshotOffset + snapshotSizeAligned > bufferSize) + throw new InvalidOperationException($"Snapshot buffer overflow during predict: snapshotOffset({snapshotOffset}) + snapshotSizeAligned({snapshotSizeAligned}) must be <= bufferSize({bufferSize})!"); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - private static void CheckSnapshotBufferSizeIsCorrect(DynamicBuffer snapshotDataBuffer, int snapshotSize) + static void CheckSnapshotBufferSizeIsCorrect(DynamicBuffer snapshotDataBuffer, int snapshotSize) { if (snapshotDataBuffer.Length != snapshotSize * GhostSystemConstants.SnapshotHistorySize) - throw new InvalidOperationException($"Invalid snapshot buffer size"); + throw new InvalidOperationException($"Invalid snapshot buffer size: snapshotDataBuffer.Length({snapshotDataBuffer.Length}) must == snapshotSize({snapshotSize}) * GhostSystemConstants.SnapshotHistorySize({GhostSystemConstants.SnapshotHistorySize})!"); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] - private void CheckPlayerIsValid() + void CheckConnectionCountIsValid() { - if (Players.Length > 1) - throw new InvalidOperationException("Ghost receive system only supports a single connection"); + if (Connections.Length > 1) + throw new InvalidOperationException($"Ghost receive system only supports a single connection: Connections.Length({Connections.Length})!"); } } @@ -1269,12 +1273,12 @@ public void OnUpdate(ref SystemState state) netStats.Data.Resize(netStats.Size, NativeArrayOptions.ClearMemory); #endif var commandBuffer = SystemAPI.GetSingleton().CreateCommandBuffer(state.WorldUnmanaged); - if (m_PlayerGroup.IsEmptyIgnoreFilter) + if (m_ConnectionsQuery.IsEmptyIgnoreFilter) { m_GhostCompletionCount[0] = m_GhostCompletionCount[1] = 0; state.CompleteDependency(); // Make sure we can access the spawned ghost map // If there were no ghosts spawned at runtime we don't need to cleanup - if (m_GhostCleanupGroup.IsEmptyIgnoreFilter && + if (m_GhostCleanupQuery.IsEmptyIgnoreFilter && m_SpawnedGhostEntityMap.Count() == 0 && m_GhostEntityMap.Count() == 0) return; var clearMapJob = new ClearMapJob @@ -1283,7 +1287,7 @@ public void OnUpdate(ref SystemState state) SpawnedGhostMap = m_SpawnedGhostEntityMap }; var clearHandle = clearMapJob.Schedule(state.Dependency); - if (!m_GhostCleanupGroup.IsEmptyIgnoreFilter) + if (!m_GhostCleanupQuery.IsEmptyIgnoreFilter) { m_EntityTypeHandle.Update(ref state); var clearJob = new ClearGhostsJob @@ -1291,7 +1295,7 @@ public void OnUpdate(ref SystemState state) EntitiesType = m_EntityTypeHandle, CommandBuffer = commandBuffer.AsParallelWriter() }; - state.Dependency = clearJob.ScheduleParallel(m_GhostCleanupGroup, state.Dependency); + state.Dependency = clearJob.ScheduleParallel(m_GhostCleanupQuery, state.Dependency); } state.Dependency = JobHandle.CombineDependencies(state.Dependency, clearHandle); return; @@ -1312,9 +1316,9 @@ public void OnUpdate(ref SystemState state) } #endif - var players = m_PlayerGroup.ToEntityListAsync(state.WorldUpdateAllocator, out var playerHandle); + var connections = m_ConnectionsQuery.ToEntityListAsync(state.WorldUpdateAllocator, out var connectionHandle); var prespawnSceneStateArray = - m_SubSceneGroup.ToComponentDataListAsync(state.WorldUpdateAllocator, + m_SubSceneQuery.ToComponentDataListAsync(state.WorldUpdateAllocator, out var prespawnHandle); ref readonly var ghostDespawnQueues = ref SystemAPI.GetSingletonRW().ValueRO; UpdateLookupsForReadStreamJob(ref state); @@ -1325,7 +1329,7 @@ public void OnUpdate(ref SystemState state) GhostTypeCollectionFromEntity = m_GhostTypeCollectionFromEntity, GhostComponentIndexFromEntity = m_GhostComponentIndexFromEntity, GhostCollectionFromEntity = m_GhostCollectionFromEntity, - Players = players, + Connections = connections, SnapshotFromEntity = m_SnapshotFromEntity, SnapshotDataBufferFromEntity = m_SnapshotDataBufferFromEntity, SnapshotDynamicDataFromEntity = m_SnapshotDynamicDataFromEntity, @@ -1359,7 +1363,7 @@ public void OnUpdate(ref SystemState state) }; var tempDeps = new NativeArray(3, Allocator.Temp); tempDeps[0] = state.Dependency; - tempDeps[1] = playerHandle; + tempDeps[1] = connectionHandle; tempDeps[2] = prespawnHandle; state.Dependency = readJob.Schedule(JobHandle.CombineDependencies(tempDeps)); } diff --git a/Runtime/Snapshot/GhostSendSystem.cs b/Runtime/Snapshot/GhostSendSystem.cs index dce562a..5f4fedf 100644 --- a/Runtime/Snapshot/GhostSendSystem.cs +++ b/Runtime/Snapshot/GhostSendSystem.cs @@ -233,7 +233,7 @@ internal void Initialize() /// /// System present only for servers worlds, and responsible to replicate ghost entities to the clients. - /// /// The is one of the most complex system of the whole package and heavily rely on multi-thread jobs to dispatch ghosts to all connection as much as possible in parallel. + /// The is one of the most complex system of the whole package and heavily rely on multi-thread jobs to dispatch ghosts to all connection as much as possible in parallel. /// /// Ghosts entities are replicated by sending a 'snapshot' of their state to the clients, at frequency. /// Snaphosts are streamed to the client when their connection is tagged with a component (we usually refere a connection with that tag as "in-game"), @@ -305,7 +305,6 @@ public partial struct GhostSendSystem : ISystem private Unity.Profiling.ProfilerMarker m_GhostGroupMarker; private GhostPreSerializer m_GhostPreSerializer; - ComponentType m_DefaultPerChunkComponentType; ComponentLookup m_NetworkIdFromEntity; ComponentLookup m_SnapshotAckFromEntity; ComponentLookup m_GhostTypeFromEntity; @@ -372,7 +371,6 @@ public void OnCreate(ref SystemState state) m_UpdateLen = new NativeArray(JobsUtility.MaxJobThreadCount, Allocator.Persistent); m_UpdateCounts = new NativeArray(JobsUtility.MaxJobThreadCount, Allocator.Persistent); #endif - m_DefaultPerChunkComponentType = ComponentType.ReadOnly(); connectionQuery = state.GetEntityQuery( ComponentType.ReadWrite(), @@ -497,10 +495,6 @@ public void OnDestroy(ref SystemState state) #endif } - struct DefaultPerChunkComponent : ISharedComponentData - { - } - [BurstCompile] struct SpawnGhostJob : IJob { @@ -547,7 +541,7 @@ public void Execute() throw new InvalidOperationException("Could not find ghost type in the collection"); if (ghostType >= GhostTypeCollection.Length) continue; // serialization data has not been loaded yet - var ghosts = spawnChunks[chunk].GetNativeArray(ghostComponentType); + var ghosts = spawnChunks[chunk].GetNativeArray(ref ghostComponentType); for (var ent = 0; ent < entities.Length; ++ent) { if (!freeGhostIds.TryDequeue(out var newId)) @@ -587,7 +581,7 @@ public void Execute() #if ENABLE_UNITY_COLLECTIONS_CHECKS if (GhostTypeCollection[ghostType].PredictionOwnerOffset != 0) { - if (!spawnChunks[chunk].Has(ghostOwnerComponentType)) + if (!spawnChunks[chunk].Has(ref ghostOwnerComponentType)) { netDebug.LogError(FixedString.Format("Ghost type is owner predicted but does not have a GhostOwnerComponent {0}, {1}", ghostType, ghostTypeComponent.guid0)); continue; @@ -595,7 +589,7 @@ public void Execute() if (GhostTypeCollection[ghostType].OwnerPredicted != 0) { // Validate that the entity has a GhostOwnerComponent and that the value in the GhosOwnerComponent has been initialized - var ghostOwners = spawnChunks[chunk].GetNativeArray(ghostOwnerComponentType); + var ghostOwners = spawnChunks[chunk].GetNativeArray(ref ghostOwnerComponentType); for (int ent = 0; ent < ghostOwners.Length; ++ent) { if (ghostOwners[ent].NetworkId == 0) @@ -760,25 +754,48 @@ public unsafe void Execute(int idx) prespawnSceneLoadedEntity, prespawnAckFromEntity, prespawnSceneLoadedFromEntity); } - var success = false; - var result = 0; - while (!success) + var serializeResult = default(SerializeEnitiesResult); + while (serializeResult != SerializeEnitiesResult.Abort && + serializeResult != SerializeEnitiesResult.Ok) { // If the requested packet size if larger than one MTU we have to use the fragmentation pipeline var pipelineToUse = (targetSnapshotSize <= maxSnapshotSizeWithoutFragmentation) ? unreliablePipeline : unreliableFragmentedPipeline; - if (driver.BeginSend(pipelineToUse, connectionId, out var dataStream, targetSnapshotSize) == 0) { - success = sendEntities(ref driver, ref dataStream, ghostChunkComponentTypesPtr, ghostChunkComponentTypesLength); - if (success) + serializeResult = SerializeEnitiesResult.Unknown; + try { - if ((result = driver.EndSend(dataStream)) < 0) + serializeResult = sendEntities(ref driver, ref dataStream, ghostChunkComponentTypesPtr, ghostChunkComponentTypesLength); + if (serializeResult == SerializeEnitiesResult.Ok) + { + var result = 0; + if ((result = driver.EndSend(dataStream)) < 0) + { + netDebug.LogWarning(FixedString.Format("An error occurred during EndSend. ErrorCode: {0}", result)); + } + } + else { - netDebug.LogWarning(FixedString.Format("An error occurred during EndSend. ErrorCode: {0}", result)); + driver.AbortSend(dataStream); } } - else - driver.AbortSend(dataStream); + finally + { + + //Finally is always called for non butsted code because there is a try-catch in outer caller (worldunmanged) + //regardless of the exception thrown (even invalidprogramexception). + //For bursted code, the try-finally has some limitation but it is still unwinding the blocks in the correct order + //(not in all cases, but it the one used here everything work fine). + //In general, the unhandled error and exceptions are all cought first by the outermost try-catch (world unmanged) + //and then the try-finally are called in reverse order (stack unwiding). + //There are two exeption handling in the ghost send system: + //- the one here, that is responsible to abort the data stream. + //- one inside the sendEntities method itself, that try to revert some internal state (i.e: the despawn ghost) + // + //The innermost finally is called first and do not abort the streams. + if (serializeResult == SerializeEnitiesResult.Unknown) + driver.AbortSend(dataStream); + } } else throw new InvalidOperationException("Failed to send a snapshot to a client"); @@ -787,7 +804,7 @@ public unsafe void Execute(int idx) } } - private unsafe bool sendEntities(ref NetworkDriver.Concurrent driver, ref DataStreamWriter dataStream, DynamicComponentTypeHandle* ghostChunkComponentTypesPtr, int ghostChunkComponentTypesLength) + private unsafe SerializeEnitiesResult sendEntities(ref NetworkDriver.Concurrent driver, ref DataStreamWriter dataStream, DynamicComponentTypeHandle* ghostChunkComponentTypesPtr, int ghostChunkComponentTypesLength) { #if NETCODE_DEBUG FixedString512Bytes debugLog = default; @@ -903,7 +920,7 @@ private unsafe bool sendEntities(ref NetworkDriver.Concurrent driver, ref DataSt if (enablePacketLogging == 1) netDebugPacket.Log("Failed to finish writing snapshot.\n"); #endif - return false; + return SerializeEnitiesResult.Failed; } #if UNITY_EDITOR || DEVELOPMENT_BUILD var netStats = netStatsBuffer.GetSubArray(netStatStride * ThreadIndex, netStatSize); @@ -960,17 +977,19 @@ private unsafe bool sendEntities(ref NetworkDriver.Concurrent driver, ref DataSt var numChunks = serialChunks.Length; if (MaxSendChunks > 0 && numChunks > MaxSendChunks) numChunks = MaxSendChunks; + + for (int pc = 0; pc < numChunks; ++pc) { var chunk = serialChunks[pc].chunk; var ghostType = serialChunks[pc].ghostType; - #if NETCODE_DEBUG serializerData.ghostTypeName = default; if (enablePacketLogging == 1) { if (prefabNamesFromEntity.HasComponent(GhostCollection[ghostType].GhostPrefab)) - serializerData.ghostTypeName.Append(prefabNamesFromEntity[GhostCollection[ghostType].GhostPrefab].Name); + serializerData.ghostTypeName.Append( + prefabNamesFromEntity[GhostCollection[ghostType].GhostPrefab].Name); } #endif @@ -979,7 +998,9 @@ private unsafe bool sendEntities(ref NetworkDriver.Concurrent driver, ref DataSt { #if NETCODE_DEBUG if (enablePacketLogging == 1) - netDebugPacket.Log(FixedString.Format("Skipping {0} in snapshot as client has not acked the spawn for it.\n", serializerData.ghostTypeName)); + netDebugPacket.Log(FixedString.Format( + "Skipping {0} in snapshot as client has not acked the spawn for it.\n", + serializerData.ghostTypeName)); #endif continue; } @@ -987,20 +1008,36 @@ private unsafe bool sendEntities(ref NetworkDriver.Concurrent driver, ref DataSt #if UNITY_EDITOR || DEVELOPMENT_BUILD var prevUpdateLen = updateLen; #endif - bool writeOK = serializerData.SerializeChunk(serialChunks[pc], ref dataStream, - ref updateLen, ref didFillPacket); + var serializeResult = default(SerializeEnitiesResult); + try + { + serializeResult = serializerData.SerializeChunk(serialChunks[pc], ref dataStream, + ref updateLen, ref didFillPacket); + } + finally + { + //If the result is unknown, an exception may have been throwm inside the serializeChunk. + if (serializeResult == SerializeEnitiesResult.Unknown) + { + //Do not abort the stream. It is aborted in the outhermost loop. + RevertDespawnGhostState(ackTick); + } + } + #if UNITY_EDITOR || DEVELOPMENT_BUILD if (updateLen > prevUpdateLen) { // indexing starts at 4 due to slots 0-3 are reserved. - netStats[ghostType*3 + 4] = netStats[ghostType*3 + 4] + updateLen - prevUpdateLen; - netStats[ghostType*3 + 5] = netStats[ghostType*3 + 5] + (uint) (dataStream.LengthInBits - startPos); - netStats[ghostType*3 + 6] = netStats[ghostType*3 + 6] + 1; // chunk count + netStats[ghostType * 3 + 4] = netStats[ghostType * 3 + 4] + updateLen - prevUpdateLen; + netStats[ghostType * 3 + 5] = + netStats[ghostType * 3 + 5] + (uint)(dataStream.LengthInBits - startPos); + netStats[ghostType * 3 + 6] = netStats[ghostType * 3 + 6] + 1; // chunk count startPos = dataStream.LengthInBits; } #endif - if (!writeOK) + if (serializeResult == SerializeEnitiesResult.Failed) break; + if (MaxSendEntities > 0) { MaxSendEntities -= chunk.Count; @@ -1008,11 +1045,12 @@ private unsafe bool sendEntities(ref NetworkDriver.Concurrent driver, ref DataSt break; } } + if (dataStream.HasFailedWrites) { RevertDespawnGhostState(ackTick); - driver.AbortSend(dataStream); - throw new InvalidOperationException("Size limitation on snapshot did not prevent all errors"); + netDebug.LogError("Size limitation on snapshot did not prevent all errors"); + return SerializeEnitiesResult.Abort; } dataStream.Flush(); @@ -1030,10 +1068,12 @@ private unsafe bool sendEntities(ref NetworkDriver.Concurrent driver, ref DataSt netDebugPacket.Log(FixedString.Format("Despawn: {0} Update:{1} {2}B\n\n", despawnLen, updateLen, dataStream.Length)); #endif - var didSend = !(didFillPacket && updateLen == 0); - if (!didSend) + if (didFillPacket && updateLen == 0) + { RevertDespawnGhostState(ackTick); - return didSend; + return SerializeEnitiesResult.Failed; + } + return SerializeEnitiesResult.Ok; } // Revert all state updates that happened from failing to write despawn packets @@ -1043,7 +1083,7 @@ void RevertDespawnGhostState(NetworkTick ackTick) ghostStateData.DespawnRepeatCount = 0; for (var chunk = 0; chunk < despawnChunks.Length; ++chunk) { - var ghostStates = despawnChunks[chunk].GetNativeArray(ghostSystemStateType); + var ghostStates = despawnChunks[chunk].GetNativeArray(ref ghostSystemStateType); for (var ent = 0; ent < ghostStates.Length; ++ent) { ref var state = ref ghostStateData.GetGhostState(ghostStates[ent]); @@ -1065,6 +1105,7 @@ void RevertDespawnGhostState(NetworkTick ackTick) } } + /// Write a list of all ghosts which have been despawned after the last acked packet. Return the number of ghost ids written uint WriteDespawnGhosts(ref DataStreamWriter dataStream, NetworkTick ackTick) { @@ -1089,7 +1130,7 @@ uint EncodeGhostId(int ghostId) uint repeatThisFrame = ghostStateData.DespawnRepeatCount; for (var chunk = 0; chunk < despawnChunks.Length; ++chunk) { - var ghostStates = despawnChunks[chunk].GetNativeArray(ghostSystemStateType); + var ghostStates = despawnChunks[chunk].GetNativeArray(ref ghostSystemStateType); for (var ent = 0; ent < ghostStates.Length; ++ent) { ref var state = ref ghostStateData.GetGhostState(ghostStates[ent]); @@ -1330,7 +1371,7 @@ NativeList GatherGhostChunks(out int maxCount, out int totalCount) maxCount = math.max(maxCount, ghostChunk.Count); //Prespawn ghost chunk should be considered only if the subscene wich they belong to as been loaded (acked) by the client. - if (ghostChunk.Has(prespawnGhostIdType)) + if (ghostChunk.Has(ref prespawnGhostIdType)) { var ackedPrespawnSceneMap = connectionState[connectionIdx].AckedPrespawnSceneMap; //Retrieve the subscene hash from the shared component index. @@ -1347,7 +1388,7 @@ NativeList GatherGhostChunks(out int maxCount, out int totalCount) } } - if (ghostChunk.Has(ghostChildEntityComponentType)) + if (ghostChunk.Has(ref ghostChildEntityComponentType)) continue; var ghostType = chunkState.ghostType; @@ -1357,11 +1398,11 @@ NativeList GatherGhostChunks(out int maxCount, out int totalCount) chunkPriority /= IrrelevantImportanceDownScale; if (chunkPriority < MinSendImportance) continue; - if (connectionHasConnectionData && connectionHasImportanceData && ghostChunk.Has(ghostImportancePerChunkTypeHandle)) + if (connectionHasConnectionData && connectionHasImportanceData && ghostChunk.Has(ref ghostImportancePerChunkTypeHandle)) { unsafe { - IntPtr chunkTile = new IntPtr(ghostChunk.GetDynamicSharedComponentDataAddress(ghostImportancePerChunkTypeHandle)); + IntPtr chunkTile = new IntPtr(ghostChunk.GetDynamicSharedComponentDataAddress(ref ghostImportancePerChunkTypeHandle)); chunkPriority = scaleGhostImportance.Ptr.Invoke(connectionDataPtr, importanceDataPtr, chunkTile, chunkPriority); } if (chunkPriority < MinDistanceScaledSendImportance) @@ -1382,7 +1423,7 @@ NativeList GatherGhostChunks(out int maxCount, out int totalCount) #endif } - var serialChunkArray = serialChunks.AsArray(); + NativeArray serialChunkArray = serialChunks.AsArray(); serialChunkArray.Sort(); return serialChunks; } @@ -1392,7 +1433,7 @@ NativeList GatherGhostChunks(out int maxCount, out int totalCount) DynamicComponentTypeHandle connectionDataTypeHandle, int typeSize) { - var ptr = (byte*)storageInfo.Chunk.GetDynamicComponentDataArrayReinterpret(connectionDataTypeHandle, typeSize).GetUnsafeReadOnlyPtr(); + var ptr = (byte*)storageInfo.Chunk.GetDynamicComponentDataArrayReinterpret(ref connectionDataTypeHandle, typeSize).GetUnsafeReadOnlyPtr(); ptr += typeSize * storageInfo.IndexInChunk; return (IntPtr)ptr; } @@ -1420,7 +1461,7 @@ private void RemoveGhostChunk(ArchetypeChunk ghostChunk, GhostChunkSerialization private bool AddNewChunk(ArchetypeChunk ghostChunk, ref GhostChunkSerializationState chunkState) { - var ghosts = ghostChunk.GetNativeArray(ghostComponentType); + var ghosts = ghostChunk.GetNativeArray(ref ghostComponentType); if (!TryGetChunkGhostType(ghostChunk, ghosts, out var chunkGhostType)) { return false; @@ -1465,7 +1506,7 @@ private bool TryGetChunkGhostType(ArchetypeChunk ghostChunk, NativeArray(ghostCollectionSingleton); @@ -1968,7 +2009,7 @@ partial struct GhostDespawnParallelJob : IJobEntity public NativeQueue.ParallelWriter FreeSpawnedGhosts; public NetworkTick CurrentTick; - public void Execute(Entity entity, [EntityInQueryIndex]int entityInQueryIndex, ref GhostCleanupComponent ghost) + public void Execute(Entity entity, [EntityIndexInQuery]int entityIndexInQuery, ref GhostCleanupComponent ghost) { var ackedByAllTick = DespawnAckedByAllTick.Value; if (!ghost.despawnTick.IsValid) @@ -1979,7 +2020,7 @@ public void Execute(Entity entity, [EntityInQueryIndex]int entityInQueryIndex, r { if (PrespawnHelper.IsRuntimeSpawnedGhost(ghost.ghostId)) FreeGhostIds.Enqueue(ghost.ghostId); - CommandBufferConcurrent.RemoveComponent(entityInQueryIndex, entity); + CommandBufferConcurrent.RemoveComponent(entityIndexInQuery, entity); } //Remove the ghost from the mapping as soon as possible, regardless of clients acknowledge var spawnedGhost = new SpawnedGhost {ghostId = ghost.ghostId, spawnTick = ghost.spawnTick}; diff --git a/Runtime/Snapshot/GhostSerializationHelper.cs b/Runtime/Snapshot/GhostSerializationHelper.cs index 262d597..273757f 100644 --- a/Runtime/Snapshot/GhostSerializationHelper.cs +++ b/Runtime/Snapshot/GhostSerializationHelper.cs @@ -64,7 +64,7 @@ private void CheckValidSnapshotOffset(int compSnapshotSize) internal void CopyComponentToSnapshot(ArchetypeChunk chunk, int ent, in GhostComponentSerializer.State serializer) { var compSize = serializer.ComponentSize; - var compData = (byte*) chunk.GetDynamicComponentDataArrayReinterpret(typeHandle, compSize).GetUnsafeReadOnlyPtr(); + var compData = (byte*) chunk.GetDynamicComponentDataArrayReinterpret(ref typeHandle, compSize).GetUnsafeReadOnlyPtr(); CheckValidSnapshotOffset(serializer.SnapshotSize); serializer.CopyToSnapshot.Ptr.Invoke((IntPtr) UnsafeUtility.AddressOf(ref serializerState), (IntPtr) snapshotPtr, snapshotOffset, snapshotSize, (IntPtr) (compData + ent * compSize), compSize, 1); @@ -103,7 +103,7 @@ public void CopyEntityToSnapshot(ArchetypeChunk chunk, int ent, in GhostCollecti CheckValidComponentIndex(compIdx); typeHandle = ghostChunkComponentTypesPtr[compIdx]; var sizeInSnapshot = GhostComponentSerializer.SizeInSnapshot(GhostComponentCollection[serializerIdx]); - if (chunk.Has(typeHandle)) + if (chunk.Has(ref typeHandle)) { if (GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { @@ -134,7 +134,7 @@ public void CopyEntityToSnapshot(ArchetypeChunk chunk, int ent, in GhostCollecti if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); var linkedEntityGroup = linkedEntityGroupAccessor[ent]; for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { @@ -144,7 +144,7 @@ public void CopyEntityToSnapshot(ArchetypeChunk chunk, int ent, in GhostCollecti typeHandle = ghostChunkComponentTypesPtr[compIdx]; var sizeInSnapshot = GhostComponentSerializer.SizeInSnapshot(GhostComponentCollection[serializerIdx]); var childEnt = linkedEntityGroup[GhostComponentIndex[typeData.FirstComponent + comp].EntityIndex].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(typeHandle)) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref typeHandle)) { if (GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { @@ -196,7 +196,7 @@ public void CopyChunkToSnapshot(ArchetypeChunk chunk, in GhostCollectionPrefabSe //It might still work in some cases but if this snapshot is then part of the history and used for //interpolated data we might get incorrect results - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var handle = ghostChunkComponentTypesPtr[compIdx]; enableableMaskOffset = GhostChunkSerializer.UpdateEnableableMasks(chunk, 0, chunk.Count, ref handle, snapshotPtr, changeMaskUints, enableableMaskOffset, snapshotSize); @@ -204,7 +204,7 @@ public void CopyChunkToSnapshot(ArchetypeChunk chunk, in GhostCollectionPrefabSe if (GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { - if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { var dynamicDataSize = GhostComponentCollection[serializerIdx].SnapshotSize; var bufData = chunk.GetUntypedBufferAccessor(ref ghostChunkComponentTypesPtr[compIdx]); @@ -234,24 +234,27 @@ public void CopyChunkToSnapshot(ArchetypeChunk chunk, in GhostCollectionPrefabSe } else { - if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (GhostComponentCollection[serializerIdx].HasGhostFields) { - var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); - GhostComponentCollection[serializerIdx].CopyToSnapshot.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), - (IntPtr)snapshotPtr, snapshotOffset, snapshotSize, (IntPtr)compData, compSize, chunk.Count); - } - else - { - for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent) - UnsafeUtility.MemClear(snapshotPtr + snapshotOffset + ent*snapshotSize, GhostComponentCollection[serializerIdx].SnapshotSize); - } + if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + { + var compData = (byte*) chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + GhostComponentCollection[serializerIdx].CopyToSnapshot.Ptr.Invoke((IntPtr) UnsafeUtility.AddressOf(ref serializerState), + (IntPtr) snapshotPtr, snapshotOffset, snapshotSize, (IntPtr) compData, compSize, chunk.Count); + } + else + { + for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent) + UnsafeUtility.MemClear(snapshotPtr + snapshotOffset + ent * snapshotSize, GhostComponentCollection[serializerIdx].SnapshotSize); + } - snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostComponentCollection[serializerIdx].SnapshotSize); + snapshotOffset += GhostComponentSerializer.SnapshotSizeAligned(GhostComponentCollection[serializerIdx].SnapshotSize); + } } } if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[typeData.FirstComponent + comp].ComponentIndex; @@ -266,7 +269,7 @@ public void CopyChunkToSnapshot(ArchetypeChunk chunk, in GhostCollectionPrefabSe { var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[typeData.FirstComponent + comp].EntityIndex].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { var bufData = childChunk.Chunk.GetUntypedBufferAccessor(ref ghostChunkComponentTypesPtr[compIdx]); var compData = (byte*)bufData.GetUnsafeReadOnlyPtrAndLength(childChunk.IndexInChunk, out var len); @@ -278,7 +281,7 @@ public void CopyChunkToSnapshot(ArchetypeChunk chunk, in GhostCollectionPrefabSe GhostComponentCollection[serializerIdx].CopyToSnapshot.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), (IntPtr)snapshotDynamicPtr, dynamicSnapshotDataOffset + maskSize, dynamicDataSize, (IntPtr)compData, compSize, len); - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var entityIndex = childChunk.IndexInChunk; var handle = ghostChunkComponentTypesPtr[compIdx]; @@ -306,15 +309,19 @@ public void CopyChunkToSnapshot(ArchetypeChunk chunk, in GhostCollectionPrefabSe var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[typeData.FirstComponent + comp].EntityIndex].Value; //We can skip here, because the memory buffer offset is computed using the start-end entity indices - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { - var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); - compData += childChunk.IndexInChunk * compSize; - // TODO: would batching be faster? - GhostComponentCollection[serializerIdx].CopyToSnapshot.Ptr.Invoke((IntPtr)UnsafeUtility.AddressOf(ref serializerState), - (IntPtr)snapshotPtr + ent*snapshotSize, snapshotOffset, snapshotSize, (IntPtr)compData, compSize, 1); + if (GhostComponentCollection[serializerIdx].HasGhostFields) + { + var compData = (byte*) childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + compData += childChunk.IndexInChunk * compSize; + + // TODO: would batching be faster? + GhostComponentCollection[serializerIdx].CopyToSnapshot.Ptr.Invoke((IntPtr) UnsafeUtility.AddressOf(ref serializerState), + (IntPtr) snapshotPtr + ent * snapshotSize, snapshotOffset, snapshotSize, (IntPtr) compData, compSize, 1); + } - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var entityIndex = childChunk.IndexInChunk; var handle = ghostChunkComponentTypesPtr[compIdx]; @@ -354,7 +361,7 @@ public int GatherBufferSize(ArchetypeChunk chunk, int startIndex, GhostCollectio { int compIdx = GhostComponentIndex[typeData.FirstComponent + comp].ComponentIndex; int serializerIdx = GhostComponentIndex[typeData.FirstComponent + comp].SerializerIndex; - if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer || !chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer || !chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) continue; for (int ent = startIndex, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent) @@ -371,7 +378,7 @@ public int GatherBufferSize(ArchetypeChunk chunk, int startIndex, GhostCollectio if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[typeData.FirstComponent + comp].ComponentIndex; @@ -384,7 +391,7 @@ public int GatherBufferSize(ArchetypeChunk chunk, int startIndex, GhostCollectio { var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[typeData.FirstComponent + comp].EntityIndex].Value; - if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { var bufferAccessor = childChunk.Chunk.GetUntypedBufferAccessor(ref ghostChunkComponentTypesPtr[compIdx]); var bufferLen = bufferAccessor.GetBufferLength(childChunk.IndexInChunk); diff --git a/Runtime/Snapshot/GhostSpawnClassificationSystem.cs b/Runtime/Snapshot/GhostSpawnClassificationSystem.cs index c18afd7..585fd29 100644 --- a/Runtime/Snapshot/GhostSpawnClassificationSystem.cs +++ b/Runtime/Snapshot/GhostSpawnClassificationSystem.cs @@ -42,7 +42,7 @@ public enum Type Interpolated, /// /// The ghost is a predicted ghost. A new ghost instance is immediately created, unless the - /// is set to a valid emtity reference, in which case the + /// is set to a valid entity reference, in which case the /// referenced entity is used instead as destination where to copy the received ghost snapshot. /// Predicted @@ -62,7 +62,7 @@ public enum Type public int GhostID; /// /// Offset im bytes used to retrieve from the temporary , present on the - /// singleton, the first received snashot from the server. + /// singleton, the first received snapshot from the server. /// public int DataOffset; /// @@ -98,20 +98,20 @@ public bool HasClassifiedPredictedSpawn } byte m_HasClassifiedPredictedSpawn; /// - /// Only valid for prepawn ghost. Mainly used by the spawning system to re-assign + /// Only valid for pre-spawned ghost. Mainly used by the spawning system to re-assign /// the PrespawnGhostIndex component to pre-spawned ghosts that has re-instantiated because of relevancy changes. /// internal int PrespawnIndex; /// - /// Only valid for prepawn ghost. The scene section that ghost belong to. + /// Only valid for pre-spawned ghost. The scene section that ghost belong to. /// internal Hash128 SceneGUID; /// - /// Only valid for prepawn ghost, used to the re-assing the correct index to the shared - /// component when an prespawned ghost is re-spawned (i.e, because of relevancy changes). + /// Only valid for pre-spawned ghost, used to the re-assign the correct index to the shared + /// component when an pre-spawned ghost is re-spawned (i.e, because of relevancy changes). /// The section index is necessary to ensure that, if the sub-scene from which the ghost were created /// is requested to be unloaded by destroying all entities that were part of the scene (the default), - /// the prespawned ghost instances are also destroyed. + /// the pre-spawned ghost instances are also destroyed. /// internal int SectionIndex; } @@ -129,13 +129,11 @@ public bool HasClassifiedPredictedSpawn [BurstCompile] public partial struct GhostSpawnClassificationSystem : ISystem { - BufferLookup m_GhostCollectionPrefabSerializerFromEntity; - + private LowLevel.SnapshotDataLookupHelper m_spawnBufferHelper; [BurstCompile] public void OnCreate(ref SystemState state) { - m_GhostCollectionPrefabSerializerFromEntity = state.GetBufferLookup(true); - + m_spawnBufferHelper = new LowLevel.SnapshotDataLookupHelper(ref state); state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(); @@ -146,10 +144,11 @@ public void OnDestroy(ref SystemState state) [BurstCompile] public void OnUpdate(ref SystemState state) { - m_GhostCollectionPrefabSerializerFromEntity.Update(ref state); + m_spawnBufferHelper.Update(ref state); var classificationJob = new GhostSpawnClassification { - ghostTypesFromEntity = m_GhostCollectionPrefabSerializerFromEntity, + helper = m_spawnBufferHelper, + spawningMap = SystemAPI.GetSingleton().Value, ghostCollectionSingleton = SystemAPI.GetSingletonEntity(), networkId = SystemAPI.GetSingleton().Value }; @@ -159,24 +158,25 @@ public void OnUpdate(ref SystemState state) [BurstCompile] partial struct GhostSpawnClassification : IJobEntity { - [ReadOnly] public BufferLookup ghostTypesFromEntity; + public LowLevel.SnapshotDataLookupHelper helper; + public NativeParallelHashMap.ReadOnly spawningMap; public Entity ghostCollectionSingleton; public int networkId; - public unsafe void Execute(DynamicBuffer ghosts, DynamicBuffer data) + public unsafe void Execute(DynamicBuffer ghosts, in DynamicBuffer data) { - var ghostTypes = ghostTypesFromEntity[ghostCollectionSingleton]; + //TODO: find way to avoid passing the spawning map and collection singleton. + var spawnBufferInspector = helper.CreateSnapshotBufferLookup(ghostCollectionSingleton, spawningMap); for (int i = 0; i < ghosts.Length; ++i) { var ghost = ghosts[i]; if (ghost.SpawnType == GhostSpawnBuffer.Type.Unknown) { - ghost.SpawnType = ghostTypes[ghost.GhostType].FallbackPredictionMode; - if (ghostTypes[ghost.GhostType].PredictionOwnerOffset != 0 && ghostTypes[ghost.GhostType].OwnerPredicted != 0) + ghost.SpawnType = spawnBufferInspector.GetFallbackPredictionMode(ghost); + if(spawnBufferInspector.IsOwnerPredicted(ghost) && spawnBufferInspector.HasGhostOwner(ghost)) { - // Prediciton mode is where the owner i is stored in the snapshot data - var dataPtr = (byte*)data.GetUnsafePtr(); - dataPtr += ghost.DataOffset; - if (*(int*)(dataPtr+ghostTypes[ghost.GhostType].PredictionOwnerOffset) == networkId) + // Prediction mode is where the owner i is stored in the snapshot data + var ghostOwner = spawnBufferInspector.GetGhostOwner(ghost, data); + if(ghostOwner == networkId) ghost.SpawnType = GhostSpawnBuffer.Type.Predicted; } ghosts[i] = ghost; @@ -202,12 +202,12 @@ internal partial struct DefaultGhostSpawnClassificationSystem : ISystem /// const uint k_TickPeriod = 5; - BufferLookup m_PredictedGhostSpawnFromEntity; + BufferLookup m_PredictedGhostSpawnLookup; [BurstCompile] public void OnCreate(ref SystemState state) { - m_PredictedGhostSpawnFromEntity = state.GetBufferLookup(); + m_PredictedGhostSpawnLookup = state.GetBufferLookup(); state.RequireForUpdate(); state.RequireForUpdate(); } @@ -218,11 +218,11 @@ public void OnCreate(ref SystemState state) [BurstCompile] public void OnUpdate(ref SystemState state) { - m_PredictedGhostSpawnFromEntity.Update(ref state); + m_PredictedGhostSpawnLookup.Update(ref state); var classificationJob = new DefaultGhostSpawnClassificationJob { spawnListEntity = SystemAPI.GetSingletonEntity(), - spawnListFromEntity = m_PredictedGhostSpawnFromEntity + spawnListLookup = m_PredictedGhostSpawnLookup }; state.Dependency = classificationJob.Schedule(state.Dependency); } @@ -232,11 +232,11 @@ public void OnUpdate(ref SystemState state) partial struct DefaultGhostSpawnClassificationJob : IJobEntity { public Entity spawnListEntity; - public BufferLookup spawnListFromEntity; + public BufferLookup spawnListLookup; public void Execute(DynamicBuffer ghosts) { - var spawnList = spawnListFromEntity[spawnListEntity]; + var spawnList = spawnListLookup[spawnListEntity]; for (int i = 0; i < ghosts.Length; ++i) { var ghost = ghosts[i]; diff --git a/Runtime/Snapshot/GhostSpawnSystem.cs b/Runtime/Snapshot/GhostSpawnSystem.cs index 87e7b82..5368bef 100644 --- a/Runtime/Snapshot/GhostSpawnSystem.cs +++ b/Runtime/Snapshot/GhostSpawnSystem.cs @@ -9,8 +9,7 @@ namespace Unity.NetCode /// /// System responsible for spawning all the ghost entities for the client world. /// - /// When a ghost snapshost that contains new ghost data is received from the server, the - /// add a spawning request to the . + /// When a ghost snapshot is received from the server, the add a spawning request to the . /// After the spawning requests has been classified (see ), /// the start processing the spawning queue. /// @@ -19,7 +18,7 @@ namespace Unity.NetCode /// When the mode is set to , the ghost creation is delayed /// until the match (or is greater) the actual spawning tick on the server. /// A temporary entity, holding the spawning information, the received snapshot data from the server, and tagged with the - /// is created. The entity will exists until the real ghost instance is spawned (or a despawn request has been received), + /// is created. The entity will exists until the real ghost instance is spawned (or a de-spawn request has been received), /// and its sole purpose of receiving new incoming snapshots (even though they are not applied to the entity, since it is not a real ghost). /// /// @@ -57,17 +56,16 @@ struct DelayedSpawnGhost public void OnCreate(ref SystemState state) { - var ent = state.EntityManager.CreateEntity(); - state.EntityManager.SetName(ent, "GhostSpawnQueue"); - state.EntityManager.AddComponentData(ent, default(GhostSpawnQueueComponent)); - state.EntityManager.AddBuffer(ent); - state.EntityManager.AddBuffer(ent); - m_DelayedInterpolatedGhostSpawnQueue = new NativeQueue(Allocator.Persistent); m_DelayedPredictedGhostSpawnQueue = new NativeQueue(Allocator.Persistent); m_InGameGroup = state.GetEntityQuery(ComponentType.ReadOnly()); m_NetworkIdQuery = state.GetEntityQuery(ComponentType.ReadOnly(), ComponentType.Exclude()); + var ent = state.EntityManager.CreateEntity(); + state.EntityManager.SetName(ent, "GhostSpawnQueue"); + state.EntityManager.AddComponentData(ent, default(GhostSpawnQueueComponent)); + state.EntityManager.AddBuffer(ent); + state.EntityManager.AddBuffer(ent); state.RequireForUpdate(); state.RequireForUpdate(); } diff --git a/Runtime/Snapshot/GhostUpdateSystem.cs b/Runtime/Snapshot/GhostUpdateSystem.cs index 4a4d005..cb6f927 100644 --- a/Runtime/Snapshot/GhostUpdateSystem.cs +++ b/Runtime/Snapshot/GhostUpdateSystem.cs @@ -1,3 +1,4 @@ +using System; using Unity.Assertions; using Unity.Entities; using Unity.Collections; @@ -106,7 +107,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE GhostTypeCollection = GhostTypeCollectionFromEntity[GhostCollectionSingleton]; GhostComponentIndex = GhostComponentIndexFromEntity[GhostCollectionSingleton]; - bool predicted = chunk.Has(predictedGhostComponentType); + bool predicted = chunk.Has(ref predictedGhostComponentType); NetworkTick targetTick = predicted ? predictedTargetTick : interpolatedTargetTick; float targetTickFraction = predicted ? predictedTargetTickFraction : interpolatedTargetTickFraction; @@ -116,16 +117,16 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE GhostOwner = ghostOwnerId, SendToOwner = SendToOwnerType.All }; - var ghostComponents = chunk.GetNativeArray(ghostType); + var ghostComponents = chunk.GetNativeArray(ref ghostType); int ghostTypeId = ghostComponents.GetFirstGhostTypeId(out var firstGhost); if (ghostTypeId < 0) return; if (ghostTypeId >= GhostTypeCollection.Length) return; // serialization data has not been loaded yet. This can only happen for prespawn objects var typeData = GhostTypeCollection[ghostTypeId]; - var ghostSnapshotDataArray = chunk.GetNativeArray(ghostSnapshotDataType); - var ghostSnapshotDataBufferArray = chunk.GetBufferAccessor(ghostSnapshotDataBufferType); - var ghostSnapshotDynamicBufferArray = chunk.GetBufferAccessor(ghostSnapshotDynamicDataBufferType); + var ghostSnapshotDataArray = chunk.GetNativeArray(ref ghostSnapshotDataType); + var ghostSnapshotDataBufferArray = chunk.GetBufferAccessor(ref ghostSnapshotDataBufferType); + var ghostSnapshotDynamicBufferArray = chunk.GetBufferAccessor(ref ghostSnapshotDynamicDataBufferType); int changeMaskUints = GhostComponentSerializer.ChangeMaskArraySizeInUInts(typeData.ChangeMaskBits); int enableableMaskUints = GhostComponentSerializer.ChangeMaskArraySizeInUInts(typeData.EnableableBits); @@ -140,9 +141,9 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var dataAtTick = new NativeArray(ghostComponents.Length, Allocator.Temp); var entityRange = new NativeList(ghostComponents.Length, Allocator.Temp); int2 nextRange = default; - var predictedGhostComponentArray = chunk.GetNativeArray(predictedGhostComponentType); + var predictedGhostComponentArray = chunk.GetNativeArray(ref predictedGhostComponentType); bool canBeStatic = typeData.StaticOptimization; - bool isPrespawn = chunk.Has(prespawnGhostIndexType); + bool isPrespawn = chunk.Has(ref prespawnGhostIndexType); // Find the ranges of entities which have data to apply, store the data to apply in an array while doing so for (int ent = firstGhost; ent < ghostComponents.Length; ++ent) { @@ -307,7 +308,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var snapshotSize = GhostComponentCollection[serializerIdx].ComponentType.IsBuffer ? GhostComponentSerializer.SnapshotSizeAligned(GhostSystemConstants.DynamicBufferComponentSnapshotSize) : GhostComponentSerializer.SnapshotSizeAligned(GhostComponentCollection[serializerIdx].SnapshotSize); - if (!chunk.Has(ghostChunkComponentTypesPtr[compIdx]) || (GhostComponentIndex[typeData.FirstComponent + comp].SendMask&requiredSendMask) == 0) + if (!chunk.Has(ref ghostChunkComponentTypesPtr[compIdx]) || (GhostComponentIndex[typeData.FirstComponent + comp].SendMask&requiredSendMask) == 0) { snapshotDataOffset += snapshotSize; continue; @@ -315,7 +316,9 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var compSize = GhostComponentCollection[serializerIdx].ComponentSize; if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { - var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + var compData = GhostComponentCollection[serializerIdx].HasGhostFields + ? (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr() + : null; deserializerState.SendToOwner = GhostComponentCollection[serializerIdx].SendToOwner; for (var rangeIdx = 0; rangeIdx < entityRange.Length; ++rangeIdx) { @@ -326,7 +329,8 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE GhostComponentCollection[serializerIdx].CopyFromSnapshot.Ptr.Invoke((System.IntPtr)UnsafeUtility.AddressOf(ref deserializerState), (System.IntPtr)snapshotData, snapshotDataOffset, snapshotDataAtTickSize, (System.IntPtr)(compData + range.x*compSize), compSize, range.y-range.x); - if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + // TODO - Move this outside the for loop. It's a batched call, so calling it n times is unnecessary. + if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { enableableMaskOffset = UpdateEnableableMask(chunk, dataAtTickPtr, changeMaskUints, enableableMaskOffset, range, ghostChunkComponentTypesPtr, compIdx); } @@ -368,7 +372,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var snapshotData = (byte*)dataAtTick.GetUnsafeReadOnlyPtr(); snapshotData += snapshotDataAtTickSize * range.x; var dataAtTickPtr = (SnapshotData.DataAtTick*) snapshotData; - if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { enableableMaskOffset = UpdateEnableableMask(chunk, dataAtTickPtr, changeMaskUints, enableableMaskOffset, range, ghostChunkComponentTypesPtr, compIdx); } @@ -378,7 +382,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE } if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[typeData.FirstComponent + comp].ComponentIndex; @@ -413,13 +417,16 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE var childChunk = childEntityLookup[childEntity]; if (!childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) continue; - var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + // We fetch these via `GetUnsafeReadOnlyPtr` only for performance reasons. It's safe. + var compData = GhostComponentCollection[serializerIdx].HasGhostFields + ? (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr() + : null; var snapshotData = (byte*)dataAtTick.GetUnsafeReadOnlyPtr(); snapshotData += snapshotDataAtTickSize * ent; GhostComponentCollection[serializerIdx].CopyFromSnapshot.Ptr.Invoke((System.IntPtr)UnsafeUtility.AddressOf(ref deserializerState), (System.IntPtr)snapshotData, snapshotDataOffset, snapshotDataAtTickSize, (System.IntPtr)(compData + childChunk.IndexInChunk*compSize), compSize, 1); var dataAtTickPtr = (SnapshotData.DataAtTick*) snapshotData; - if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var childRange = new int2 { x = childChunk.IndexInChunk, y = childChunk.IndexInChunk + 1 }; UpdateEnableableMask(childChunk.Chunk, dataAtTickPtr, changeMaskUints, maskOffset, childRange, ghostChunkComponentTypesPtr, compIdx); @@ -445,7 +452,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE if (!childEntityLookup.Exists(childEntity)) continue; var childChunk = childEntityLookup[childEntity]; - if (!childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (!childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) continue; //Compute the required owner mask for the buffers and skip the copyfromsnapshot. The check must be done @@ -468,7 +475,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE (System.IntPtr) UnsafeUtility.AddressOf(ref dynamicDataAtTick), 0, dynamicDataSize, componentData, compSize, bufLen); - if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (typeData.EnableableBits > 0 && GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { var snapshotData = (byte*)dataAtTick.GetUnsafeReadOnlyPtr(); snapshotData += snapshotDataAtTickSize * ent; @@ -486,6 +493,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE } } + // TODO - We can perform this logic faster using the EnabledMask. private static int UpdateEnableableMask(ArchetypeChunk chunk, SnapshotData.DataAtTick* dataAtTickPtr, int changeMaskUints, int enableableMaskOffset, int2 range, DynamicComponentTypeHandle* ghostChunkComponentTypesPtr, int compIdx) @@ -579,9 +587,9 @@ bool RestorePredictionBackup(ArchetypeChunk chunk, int ent, in GhostCollectionPr if ((GhostComponentIndex[baseOffset + comp].SendMask&requiredSendMask) == 0) continue; - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { - if (chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { bool isSet = (enabledBitPtr[ent>>6] & (1ul<<(ent&0x3f))) != 0; chunk.SetComponentEnabled(ref ghostChunkComponentTypesPtr[compIdx], ent, isSet); @@ -592,7 +600,7 @@ bool RestorePredictionBackup(ArchetypeChunk chunk, int ent, in GhostCollectionPr var compSize = GhostComponentCollection[serializerIdx].ComponentType.IsBuffer ? GhostSystemConstants.DynamicBufferComponentSnapshotSize : GhostComponentCollection[serializerIdx].ComponentSize; - if (!chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + if (compSize == 0 || !chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { dataPtr = PredictionBackupState.GetNextData(dataPtr, compSize, chunk.Capacity); continue; @@ -606,7 +614,7 @@ bool RestorePredictionBackup(ArchetypeChunk chunk, int ent, in GhostCollectionPr if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { - var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); GhostComponentCollection[serializerIdx].RestoreFromBackup.Ptr.Invoke((System.IntPtr)(compData + ent * compSize), (System.IntPtr)(dataPtr + ent * compSize)); } else @@ -643,7 +651,7 @@ bool RestorePredictionBackup(ArchetypeChunk chunk, int ent, in GhostCollectionPr } if (typeData.NumChildComponents > 0) { - var linkedEntityGroupAccessor = chunk.GetBufferAccessor(linkedEntityGroupType); + var linkedEntityGroupAccessor = chunk.GetBufferAccessor(ref linkedEntityGroupType); for (int comp = numBaseComponents; comp < typeData.NumComponents; ++comp) { int compIdx = GhostComponentIndex[baseOffset + comp].ComponentIndex; @@ -662,10 +670,10 @@ bool RestorePredictionBackup(ArchetypeChunk chunk, int ent, in GhostCollectionPr var linkedEntityGroup = linkedEntityGroupAccessor[ent]; var childEnt = linkedEntityGroup[GhostComponentIndex[typeData.FirstComponent + comp].EntityIndex].Value; - if (GhostComponentCollection[serializerIdx].ComponentType.IsEnableable) + if (GhostComponentCollection[serializerIdx].SerializesEnabledBit != 0) { if (childEntityLookup.TryGetValue(childEnt, out var enabledChildChunk) && - enabledChildChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + enabledChildChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { bool isSet = (enabledBitPtr[ent>>6] & (1ul<<(ent&0x3f))) != 0; enabledChildChunk.Chunk.SetComponentEnabled(ref ghostChunkComponentTypesPtr[compIdx], enabledChildChunk.IndexInChunk, isSet); @@ -673,18 +681,18 @@ bool RestorePredictionBackup(ArchetypeChunk chunk, int ent, in GhostCollectionPr enabledBitPtr = PredictionBackupState.GetNextEnabledBits(enabledBitPtr, chunk.Capacity); } - if ((GhostComponentCollection[serializerIdx].SendToOwner & requiredOwnerMask) == 0) + if (compSize == 0 || (GhostComponentCollection[serializerIdx].SendToOwner & requiredOwnerMask) == 0) { dataPtr = PredictionBackupState.GetNextData(dataPtr, compSize, chunk.Capacity); continue; } if (childEntityLookup.TryGetValue(childEnt, out var childChunk) && - childChunk.Chunk.Has(ghostChunkComponentTypesPtr[compIdx])) + childChunk.Chunk.Has(ref ghostChunkComponentTypesPtr[compIdx])) { if (!GhostComponentCollection[serializerIdx].ComponentType.IsBuffer) { - var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); + var compData = (byte*)childChunk.Chunk.GetDynamicComponentDataArrayReinterpret(ref ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); GhostComponentCollection[serializerIdx].RestoreFromBackup.Ptr.Invoke((System.IntPtr)(compData + childChunk.IndexInChunk * compSize), (System.IntPtr)(dataPtr + ent * compSize)); } else @@ -727,9 +735,9 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE // This job is not written to support queries with enableable component types. Assert.IsFalse(useEnabledMask); - var owners = chunk.GetNativeArray(ghostOwnerType); + var owners = chunk.GetNativeArray(ref ghostOwnerType); for (int i = 0; i < owners.Length; ++i) - chunk.SetComponentEnabled(ghostOwnerIsLocalType, i, owners[i].NetworkId == localNetworkId); + chunk.SetComponentEnabled(ref ghostOwnerIsLocalType, i, owners[i].NetworkId == localNetworkId); } } diff --git a/Runtime/Snapshot/PredictedGhostSpawnSystem.cs b/Runtime/Snapshot/PredictedGhostSpawnSystem.cs index 48682c1..a310995 100644 --- a/Runtime/Snapshot/PredictedGhostSpawnSystem.cs +++ b/Runtime/Snapshot/PredictedGhostSpawnSystem.cs @@ -106,9 +106,9 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo DynamicComponentTypeHandle* ghostChunkComponentTypesPtr = DynamicTypeList.GetData(); var entityList = chunk.GetNativeArray(entityType); - var snapshotDataList = chunk.GetNativeArray(snapshotDataType); - var snapshotDataBufferList = chunk.GetBufferAccessor(snapshotDataBufferType); - var snapshotDynamicDataBufferList = chunk.GetBufferAccessor(snapshotDynamicDataBufferType); + var snapshotDataList = chunk.GetNativeArray(ref snapshotDataType); + var snapshotDataBufferList = chunk.GetBufferAccessor(ref snapshotDataBufferType); + var snapshotDynamicDataBufferList = chunk.GetBufferAccessor(ref snapshotDynamicDataBufferType); var GhostCollection = GhostCollectionFromEntity[GhostCollectionSingleton]; var GhostTypeCollection = GhostTypeCollectionFromEntity[GhostCollectionSingleton]; diff --git a/Runtime/Snapshot/Prespawn/AutoTrackPrespawnSection.cs b/Runtime/Snapshot/Prespawn/AutoTrackPrespawnSection.cs index 64df45b..6846e54 100644 --- a/Runtime/Snapshot/Prespawn/AutoTrackPrespawnSection.cs +++ b/Runtime/Snapshot/Prespawn/AutoTrackPrespawnSection.cs @@ -81,12 +81,13 @@ public void OnUpdate(ref SystemState state) }; state.Dependency = ackJob.Schedule(state.Dependency); } + [BurstCompile] partial struct ClientPrespawnAck : IJobEntity { [ReadOnly] public ComponentLookup sectionLoadedFromEntity; public NetDebug netDebug; public EntityCommandBuffer entityCommandBuffer; - public void Execute(Entity entity, [EntityInQueryIndex] int entityInQueryIndex, ref SubSceneWithGhostStateComponent stateComponent) + public void Execute(Entity entity, ref SubSceneWithGhostStateComponent stateComponent) { bool isLoaded = sectionLoadedFromEntity.HasComponent(entity); if (!isLoaded && stateComponent.Streaming != 0) diff --git a/Runtime/Snapshot/Prespawn/ClientTrackLoadedPrespawnSections.cs b/Runtime/Snapshot/Prespawn/ClientTrackLoadedPrespawnSections.cs index cdeaffb..3b95b05 100644 --- a/Runtime/Snapshot/Prespawn/ClientTrackLoadedPrespawnSections.cs +++ b/Runtime/Snapshot/Prespawn/ClientTrackLoadedPrespawnSections.cs @@ -86,6 +86,7 @@ public void OnUpdate(ref SystemState state) }; state.Dependency = removeJob.Schedule(state.Dependency); } + [BurstCompile] struct RemovePrespawnedGhosts : IJob { public NativeList ghostsToRemove; diff --git a/Runtime/Snapshot/Prespawn/PrespawnGhostInitializationSystem.cs b/Runtime/Snapshot/Prespawn/PrespawnGhostInitializationSystem.cs index 91ef73b..e2581ff 100644 --- a/Runtime/Snapshot/Prespawn/PrespawnGhostInitializationSystem.cs +++ b/Runtime/Snapshot/Prespawn/PrespawnGhostInitializationSystem.cs @@ -257,6 +257,7 @@ public void OnUpdate(ref SystemState state) commandBuffer.Dispose(); } + [BurstCompile] struct AggregateHash : IJob { public NativeList baselinesHashes; diff --git a/Runtime/Snapshot/Prespawn/PrespawnGhostJobs.cs b/Runtime/Snapshot/Prespawn/PrespawnGhostJobs.cs index 654c8a9..bc24e4e 100644 --- a/Runtime/Snapshot/Prespawn/PrespawnGhostJobs.cs +++ b/Runtime/Snapshot/Prespawn/PrespawnGhostJobs.cs @@ -47,7 +47,7 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo var entities = chunk.GetNativeArray(entityType); var GhostCollection = GhostCollectionFromEntity[GhostCollectionSingleton]; var GhostTypeCollection = GhostTypeCollectionFromEntity[GhostCollectionSingleton]; - var ghostTypeComponent = chunk.GetNativeArray(ghostTypeComponentType)[0]; + var ghostTypeComponent = chunk.GetNativeArray(ref ghostTypeComponentType)[0]; int ghostType; for (ghostType = 0; ghostType < GhostCollection.Length; ++ghostType) { @@ -84,7 +84,7 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo int enableableMaskUints = GhostComponentSerializer.ChangeMaskArraySizeInUInts(typeData.EnableableBits); var snapshotBaseOffset = GhostComponentSerializer.SnapshotSizeAligned(sizeof(uint) + changeMaskUints*sizeof(uint) + enableableMaskUints*sizeof(uint)); - var bufferAccessor = chunk.GetBufferAccessor(prespawnBaseline); + var bufferAccessor = chunk.GetBufferAccessor(ref prespawnBaseline); var chunkHashes = stackalloc ulong[entities.Length]; for (int i = 0; i < entities.Length; ++i) { @@ -137,7 +137,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE // This job is not written to support queries with enableable component types. Assert.IsFalse(useEnabledMask); - var ghostTypes = chunk.GetNativeArray(ghostTypeHandle); + var ghostTypes = chunk.GetNativeArray(ref ghostTypeHandle); if (!prefabFromType.TryGetValue(ghostTypes[0], out var ghostPrefabEntity)) { netDebug.LogError("Failed to look up ghost type"); @@ -151,7 +151,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE } ref var ghostMetaData = ref metaDataFromEntity[ghostPrefabEntity].Value.Value; - var linkedEntityBufferAccessor = chunk.GetBufferAccessor(linkedEntityTypeHandle); + var linkedEntityBufferAccessor = chunk.GetBufferAccessor(ref linkedEntityTypeHandle); for (int index = 0, chunkEntityCount = chunk.Count; index < chunkEntityCount; ++index) { @@ -222,9 +222,9 @@ public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bo Assert.IsFalse(useEnabledMask); var entities = chunk.GetNativeArray(entityType); - var preSpawnedIds = chunk.GetNativeArray(prespawnIdType); - var ghostComponents = chunk.GetNativeArray(ghostComponentType); - var ghostStates = chunk.GetNativeArray(ghostStateTypeHandle); + var preSpawnedIds = chunk.GetNativeArray(ref prespawnIdType); + var ghostComponents = chunk.GetNativeArray(ref ghostComponentType); + var ghostStates = chunk.GetNativeArray(ref ghostStateTypeHandle); var chunkSpawnedGhostMappings = stackalloc SpawnedGhostMapping[chunk.Count]; int spawnedGhostCount = 0; diff --git a/Runtime/Snapshot/Prespawn/PrespawnHelper.cs b/Runtime/Snapshot/Prespawn/PrespawnHelper.cs index 531ac48..5743f4b 100644 --- a/Runtime/Snapshot/Prespawn/PrespawnHelper.cs +++ b/Runtime/Snapshot/Prespawn/PrespawnHelper.cs @@ -88,7 +88,7 @@ static public void PopulateSceneHashLookupTable(EntityQuery query, EntityManager hashMap.Clear(); for (int i = 0; i < chunks.Length; ++i) { - var sharedComponentIndex = chunks[i].GetSharedComponentIndex(sharedComponentType); + var sharedComponentIndex = chunks[i].GetSharedComponentIndex(ref sharedComponentType); var sharedComponentValue = entityManager.GetSharedComponent(sharedComponentIndex); hashMap.TryAdd(sharedComponentIndex, sharedComponentValue.Value); } diff --git a/Runtime/Snapshot/Prespawn/ServerPopulatePrespawnedGhosts.cs b/Runtime/Snapshot/Prespawn/ServerPopulatePrespawnedGhosts.cs index 915c5e9..ff87e1c 100644 --- a/Runtime/Snapshot/Prespawn/ServerPopulatePrespawnedGhosts.cs +++ b/Runtime/Snapshot/Prespawn/ServerPopulatePrespawnedGhosts.cs @@ -189,6 +189,7 @@ public void OnUpdate(ref SystemState state) state.Dependency = addJob.Schedule(state.Dependency); } + [BurstCompile] struct ServerAddPrespawn : IJob { public NetDebug netDebug; diff --git a/Runtime/Snapshot/Prespawn/ServerTrackLoadedPrespawnSections.cs b/Runtime/Snapshot/Prespawn/ServerTrackLoadedPrespawnSections.cs index bc925c9..7297468 100644 --- a/Runtime/Snapshot/Prespawn/ServerTrackLoadedPrespawnSections.cs +++ b/Runtime/Snapshot/Prespawn/ServerTrackLoadedPrespawnSections.cs @@ -123,6 +123,8 @@ public void OnUpdate(ref SystemState state) if(subsceneCollection.Length == 0 && m_AllPrespawnScenes.IsEmpty) entityCommandBuffer.DestroyEntity(SystemAPI.GetSingletonEntity()); } + + [BurstCompile] struct PrespawnSceneCleanup : IJob { public NativeList unloadedGhostRange; diff --git a/Runtime/Snapshot/SnapshotDataBufferComponentLookup.cs b/Runtime/Snapshot/SnapshotDataBufferComponentLookup.cs new file mode 100644 index 0000000..86c9d49 --- /dev/null +++ b/Runtime/Snapshot/SnapshotDataBufferComponentLookup.cs @@ -0,0 +1,392 @@ +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Mathematics; +using Unity.NetCode.LowLevel.Unsafe; + +namespace Unity.NetCode.LowLevel +{ + /// + /// Helper struct that can be used in your spawn classification systems (and classification + /// jobs) to create instances. + /// + public struct SnapshotDataLookupHelper + { + [ReadOnly] private BufferLookup m_GhostCollectionPrefabSerializerLookup; + [ReadOnly] private BufferLookup m_GhostCollectionComponentIndexLookup; + [ReadOnly] private BufferLookup m_GhostCollectionComponentTypeLookup; + [ReadOnly] private BufferLookup m_GhostCollectionSerializersLookup; + [ReadOnly] private ComponentLookup m_ComponentCacheLookup; + + /// + /// Default constructor, collect and initialize all the internal handles. + /// + /// + public SnapshotDataLookupHelper(ref SystemState state) + { + m_GhostCollectionPrefabSerializerLookup = state.GetBufferLookup(true); + m_GhostCollectionComponentIndexLookup = state.GetBufferLookup(true); + m_GhostCollectionComponentTypeLookup = state.GetBufferLookup(true); + m_GhostCollectionSerializersLookup = state.GetBufferLookup(true); + m_ComponentCacheLookup = state.GetComponentLookup(); + } + + /// + /// Call this method in your system OnUpdate to refresh all the internal handles. + /// + /// + public void Update(ref SystemState state) + { + m_GhostCollectionPrefabSerializerLookup.Update(ref state); + m_GhostCollectionComponentIndexLookup.Update(ref state); + m_GhostCollectionComponentTypeLookup.Update(ref state); + m_GhostCollectionSerializersLookup.Update(ref state); + m_ComponentCacheLookup.Update(ref state); + } + + /// + /// Create a new instance. + /// + /// + /// The method requires that the method has been called and that all the internal handles + /// has been updated. + /// + /// Singleton entity containing the GhostCollectionComponent lookups. + /// Pass the existing map. + /// A valid instance + public SnapshotDataBufferComponentLookup CreateSnapshotBufferLookup(Entity ghostCollectionSingleton, in NativeParallelHashMap.ReadOnly ghostMap) + { + return new SnapshotDataBufferComponentLookup( + m_GhostCollectionPrefabSerializerLookup[ghostCollectionSingleton], + m_GhostCollectionComponentIndexLookup[ghostCollectionSingleton], + m_GhostCollectionComponentTypeLookup[ghostCollectionSingleton], + m_GhostCollectionSerializersLookup[ghostCollectionSingleton], + m_ComponentCacheLookup[ghostCollectionSingleton].ComponentDataOffsets, + ghostMap); + } + } + + /// + /// Helper struct that can be used to inspect the presence of components from a buffer + /// and retrieve their data. + /// + /// The helper allow to only read component data. Buffers are not supported. + /// + /// + public struct SnapshotDataBufferComponentLookup + { + private DynamicBuffer m_ghostPrefabType; + private DynamicBuffer m_ghostComponentIndices; + private DynamicBuffer m_ghostComponentTypes; + private DynamicBuffer m_ghostSerializers; + private readonly NativeParallelHashMap.ReadOnly m_ghostMap; + private NativeHashMap m_componentOffsetCacheRW; + + internal SnapshotDataBufferComponentLookup( + in DynamicBuffer ghostPrefabType, + in DynamicBuffer ghostComponentIndices, + in DynamicBuffer ghostComponentTypes, + in DynamicBuffer ghostSerializers, + in NativeHashMap componentOffsetCache, + in NativeParallelHashMap.ReadOnly ghostMap) + { + m_ghostPrefabType = ghostPrefabType; + m_ghostComponentIndices = ghostComponentIndices; + m_ghostComponentTypes = ghostComponentTypes; + m_ghostSerializers = ghostSerializers; + m_componentOffsetCacheRW = componentOffsetCache; + m_ghostMap = ghostMap; + } + + /// + /// Check if the spawning ghost mode is owner predicted. + /// + /// + /// True if the spawning ghost is owner predicted + public bool IsOwnerPredicted(in GhostSpawnBuffer ghost) + { + return m_ghostPrefabType.ElementAtRO(ghost.GhostType).OwnerPredicted != 0; + } + + /// + /// Check if the spawning ghost has a . + /// + /// + /// True if the spawning ghost is owner predicted + public bool HasGhostOwner(in GhostSpawnBuffer ghost) + { + return m_ghostPrefabType.ElementAtRO(ghost.GhostType).PredictionOwnerOffset != 0; + } + + /// + /// Retrieve the network id of the player owning the ghost if the ghost archetype has a + /// . + /// + /// + /// + /// the id of the player owning the ghost, if the is present, 0 otherwise. + public int GetGhostOwner(in GhostSpawnBuffer ghost, in DynamicBuffer data) + { + ref readonly var ghostPrefabSerializer = ref m_ghostPrefabType.ElementAtRO(ghost.GhostType); + if (ghostPrefabSerializer.PredictionOwnerOffset != 0) + { + unsafe + { + var dataPtr = (byte*)data.GetUnsafeReadOnlyPtr() + ghost.DataOffset; + return *(int*)(dataPtr + ghostPrefabSerializer.PredictionOwnerOffset); + } + } + return 0; + } + + /// + /// Retrieve the prediction mode used as fallback if the spawning ghost has not been + /// classified. + /// + /// + /// The fallback mode to use + public GhostSpawnBuffer.Type GetFallbackPredictionMode(in GhostSpawnBuffer ghost) + { + return m_ghostPrefabType.ElementAtRO(ghost.GhostType).FallbackPredictionMode; + } + + /// + /// Check if the a component of type is present this spawning ghost. + /// + /// The index in the collection + /// + /// + //This work for both IComponentData and IBufferElementData + public bool HasComponent(int ghostTypeIndex) where T: unmanaged, IComponentData + { + return GetComponentDataOffset(TypeManager.GetTypeIndex(), ghostTypeIndex, out _) >= 0; + } + + /// + /// Check if the a component of type is present this spawning ghost. + /// + /// The index in the collection + /// + /// + //This work for both IComponentData and IBufferElementData + public bool HasBuffer(int ghostTypeIndex) where T: unmanaged, IBufferElementData + { + return GetComponentDataOffset(TypeManager.GetTypeIndex(), ghostTypeIndex, out _) >= 0; + } + + /// + /// Try to retrieve the data for a component type from the the snapshot history buffer. + /// + /// + /// Buffers aren't supported. + /// + /// Only component present on the root entity can be retrieved. Trying to get data for component in a child entity is not supported. + /// + /// + /// The index in the collection. + /// The entity snapshot history buffer. + /// The deserialized component data. + /// The slot in the history buffer to use. + /// + /// True if the component is present and the component data is initialized. False otherwise + public bool TryGetComponentDataFromSnapshotHistory(int ghostTypeIndex, in DynamicBuffer snapshotBuffer, + out T componentData, int slotIndex=0) where T : unmanaged, IComponentData + { + componentData = default; + var offset = GetComponentDataOffset(TypeManager.GetTypeIndex(), ghostTypeIndex, out var serializerIndex); + if (offset < 0) + return false; + + var snapshotSize = m_ghostPrefabType.ElementAtRO(ghostTypeIndex).SnapshotSize; + var dataOffset = snapshotSize * slotIndex; +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (snapshotSize > 0 && dataOffset > (snapshotBuffer.Length - snapshotSize) ) + throw new System.IndexOutOfRangeException($"Cannot read component data from the snapshot buffer at index {slotIndex}. The snapshot buffer has {snapshotBuffer.Length/snapshotSize} slots."); +#endif + CopyDataFromSnapshot(snapshotBuffer, dataOffset + offset, serializerIndex, ref componentData); + return true; + } + + /// + /// Try to retrieve the data for a component type from the spawning buffer. + /// + /// + /// Buffers aren't supported. + /// + /// Only component present on the root entity can be retrieved. Trying to get data for component in a child entity is not supported. + /// + /// + /// + /// + /// + /// + /// True if the component is present and the component data is initialized. False otherwise + public bool TryGetComponentDataFromSpawnBuffer(in GhostSpawnBuffer ghost, + in DynamicBuffer snapshotData, out T componentData) where T: unmanaged, IComponentData + { + componentData = default; + var offset = GetComponentDataOffset(TypeManager.GetTypeIndex(), ghost.GhostType, out var serializerIndex); + if (offset < 0) + return false; + CopyDataFromSnapshot(snapshotData, ghost.DataOffset + offset, serializerIndex, ref componentData); + return true; + } + + private unsafe void CopyDataFromSnapshot(DynamicBuffer historyBuffer, int dataOffset, + int serializerIndex , ref T componentData) where T : unmanaged, IComponentData + { + //From here retrieving the data requires the serializer for this component type and ghost + ref readonly var serializer = ref m_ghostSerializers.ElementAtRO(serializerIndex); + //Force copy the type, not matter what the client filter is. Worst scenario, the component + //has the default data (as it should be). + var deserializerState = new GhostDeserializerState + { + GhostMap = m_ghostMap, + SendToOwner = SendToOwnerType.All + }; + //TODO: we may eventually use a more specialized version of this function that does less things and specifically designed for that + var compDataPtr = (byte*)historyBuffer.GetUnsafeReadOnlyPtr() + dataOffset; + var dataAtTick = new SnapshotData.DataAtTick + { + SnapshotBefore = (System.IntPtr)compDataPtr, + SnapshotAfter = (System.IntPtr)compDataPtr, + GhostOwner = 0 + }; + m_ghostSerializers[serializerIndex].CopyFromSnapshot.Ptr.Invoke( + (System.IntPtr)UnsafeUtility.AddressOf(ref deserializerState), + (System.IntPtr)UnsafeUtility.AddressOf(ref dataAtTick), + 0, + 0, + (System.IntPtr)UnsafeUtility.AddressOf(ref componentData), serializer.ComponentSize, + 1); + } + + //The offset for the component, along with its serializer that the user wants to inspect are cached by the GetComponentDataOffset. + //There were two options when to cache this information: + //- when we process the prefab if we know that a component type should be "inspected" (maybe an attribute?? or registered to the collection) + //- by caching on demand the result of this function, by providing a a small cache of (ghost-type, component-type) pairs. + // + //In order to pre-cache that information during prefab processing we need to provide some API (registration or attribute for code gen), + //to declare which component CAN be inspected. + //For sake of simplicity is done here, on demand and only if necessary. + //Why not caching this into the GhostCollectionPrefabType ? + //In general, users need to inspect the ghost buffer to resolve and classify pre-spawned ghosts (pretty much). + //If you have 1000 prefabs, how many of them can be "predicatively spawned"? not many probably. It is safe to assume + //that this cache will be small in general, and not needed for the majority of the prefabs. + //On the other end, we are also not expecting many component types need to be inspected either. Maybe 1 or 2 custom + //component are used in a whole project to uniquely identifying a spawn. + //But because the bound is not well known yet (assumption need data support), it is better to be a little more flexible. + private int GetComponentDataOffset(int typeIndex, int ghostType, out int serializerIndex) + { + if (!m_componentOffsetCacheRW.IsCreated) + return FindSerializerIndexAndComponentDataOffset(typeIndex, ghostType, out serializerIndex); + + var key = new SnapshotLookupCacheKey(typeIndex, ghostType); + if (!m_componentOffsetCacheRW.TryGetValue(key, out var cachedOffset)) + { + cachedOffset.dataOffset = FindSerializerIndexAndComponentDataOffset(typeIndex, ghostType, out cachedOffset.serializerIndex); + m_componentOffsetCacheRW.Add(key, cachedOffset); + } + serializerIndex = cachedOffset.serializerIndex; + return cachedOffset.dataOffset; + } + + //The calculated offset comprises also the initial snapshot header (that depend on the ghost type). + private int FindSerializerIndexAndComponentDataOffset(int typeIndex, int ghostType, out int compSerializerIndex) + { + var prefabType = m_ghostPrefabType.ElementAtRO(ghostType); + var offset = GhostComponentSerializer.SnapshotHeaderSizeInBytes(prefabType); + for (var i = 0; i < prefabType.NumComponents; ++i) + { + ref readonly var compIndices = ref m_ghostComponentIndices.ElementAtRO(prefabType.FirstComponent + i); ; + var comType = m_ghostComponentTypes.ElementAtRO(compIndices.ComponentIndex).Type; + if (comType.TypeIndex == typeIndex) + { + compSerializerIndex = compIndices.SerializerIndex; + return offset; + } + var compSize = comType.IsBuffer + ? GhostSystemConstants.DynamicBufferComponentSnapshotSize + : m_ghostSerializers.ElementAtRO(compIndices.SerializerIndex).SnapshotSize; + offset += GhostComponentSerializer.SnapshotSizeAligned(compSize); + } + //Not found + compSerializerIndex = default; + return -1; + } + } + + internal struct SnapshotLookupCacheKey : System.IEquatable + { + public int ghostType; + public int typeIndex; + + public SnapshotLookupCacheKey(int ghostType, int typeIndex) + { + this.ghostType = ghostType; + this.typeIndex = typeIndex; + } + + public bool Equals(SnapshotLookupCacheKey other) + { + return ghostType == other.ghostType && typeIndex == other.typeIndex; + } + + public override bool Equals(object obj) + { + return obj is SnapshotLookupCacheKey other && Equals(other); + } + + public override int GetHashCode() + { + return (int)math.hash(new int2(ghostType, typeIndex)); + } + } + + /// + /// Add to the GhostCollection singleton a new component that is used + /// by the . + /// + [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] + [CreateAfter(typeof(GhostCollectionSystem))] + internal partial struct SnapshotLookupCacheSystem : ISystem + { + /// + /// Maps a given component and ghost type pairs to the data offset inside the snapshot. + /// + private NativeHashMap m_SnapshotDataLookupCache; + + public void OnCreate(ref SystemState state) + { + m_SnapshotDataLookupCache = new NativeHashMap(128, Allocator.Persistent); + var collection = SystemAPI.GetSingletonEntity(); + state.EntityManager.AddComponentData(collection, new SnapshotDataLookupCache + { + ComponentDataOffsets = m_SnapshotDataLookupCache + }); + state.Enabled = false; + } + public void OnDestroy(ref SystemState state) + { + m_SnapshotDataLookupCache.Dispose(); + } + public void OnUpdate(ref SystemState state) + { + } + } + + /// + /// Component added singleton entity, used internally + /// to cache the offset of the inspected component in the snapshot buffer for the different ghost types. + /// + internal struct SnapshotDataLookupCache : IComponentData + { + public struct SerializerIndexAndOffset + { + public int serializerIndex; + public int dataOffset; + } + internal NativeHashMap ComponentDataOffsets; + } +} + diff --git a/Runtime/Snapshot/SnapshotDataBufferComponentLookup.cs.meta b/Runtime/Snapshot/SnapshotDataBufferComponentLookup.cs.meta new file mode 100644 index 0000000..3616c96 --- /dev/null +++ b/Runtime/Snapshot/SnapshotDataBufferComponentLookup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6715eb5f88da45e4aae28848564d7305 +timeCreated: 1657889737 \ No newline at end of file diff --git a/Runtime/Snapshot/SwitchPredictionSmoothingSystem.cs b/Runtime/Snapshot/SwitchPredictionSmoothingSystem.cs index 097b2b1..adad249 100644 --- a/Runtime/Snapshot/SwitchPredictionSmoothingSystem.cs +++ b/Runtime/Snapshot/SwitchPredictionSmoothingSystem.cs @@ -46,18 +46,27 @@ public struct SwitchPredictionSmoothing : IComponentData /// /// [UpdateInGroup(typeof(TransformSystemGroup))] +#if !ENABLE_TRANSFORM_V1 + [UpdateBefore(typeof(LocalToWorldSystem))] +#else [UpdateBefore(typeof(TRSToLocalToWorldSystem))] +#endif [BurstCompile] public partial struct SwitchPredictionSmoothingSystem : ISystem { EntityQuery m_SwitchPredictionSmoothingQuery; EntityTypeHandle m_EntityTypeHandle; +#if !ENABLE_TRANSFORM_V1 + ComponentTypeHandle m_TransformHandle; + ComponentTypeHandle m_PostTransformScaleType; +#else ComponentTypeHandle m_TranslationHandle; ComponentTypeHandle m_RotationHandle; ComponentTypeHandle m_NonUniformScaleHandle; ComponentTypeHandle m_ScaleHandle; ComponentTypeHandle m_CompositeScaleHandle; +#endif ComponentTypeHandle m_SwitchPredictionSmoothingHandle; ComponentTypeHandle m_LocalToWorldHandle; @@ -65,17 +74,26 @@ public partial struct SwitchPredictionSmoothingSystem : ISystem public void OnCreate(ref SystemState state) { var builder = new EntityQueryBuilder(Allocator.Temp) +#if !ENABLE_TRANSFORM_V1 + .WithAll() +#else .WithAll() +#endif .WithAllRW(); m_SwitchPredictionSmoothingQuery = state.GetEntityQuery(builder); state.RequireForUpdate(m_SwitchPredictionSmoothingQuery); m_EntityTypeHandle = state.GetEntityTypeHandle(); +#if !ENABLE_TRANSFORM_V1 + m_TransformHandle = state.GetComponentTypeHandle(true); + m_PostTransformScaleType = state.GetComponentTypeHandle(true); +#else m_TranslationHandle = state.GetComponentTypeHandle(true); m_RotationHandle = state.GetComponentTypeHandle(true); m_NonUniformScaleHandle = state.GetComponentTypeHandle(true); m_ScaleHandle = state.GetComponentTypeHandle(true); m_CompositeScaleHandle = state.GetComponentTypeHandle(true); +#endif m_SwitchPredictionSmoothingHandle = state.GetComponentTypeHandle(); m_LocalToWorldHandle = state.GetComponentTypeHandle(); } @@ -91,22 +109,32 @@ public void OnUpdate(ref SystemState state) var commandBuffer = SystemAPI.GetSingleton().CreateCommandBuffer(state.WorldUnmanaged); m_EntityTypeHandle.Update(ref state); +#if !ENABLE_TRANSFORM_V1 + m_TransformHandle.Update(ref state); + m_PostTransformScaleType.Update(ref state); +#else m_TranslationHandle.Update(ref state); m_RotationHandle.Update(ref state); m_NonUniformScaleHandle.Update(ref state); m_ScaleHandle.Update(ref state); m_CompositeScaleHandle.Update(ref state); +#endif m_SwitchPredictionSmoothingHandle.Update(ref state); m_LocalToWorldHandle.Update(ref state); state.Dependency = new SwitchPredictionSmoothingJob { EntityType = m_EntityTypeHandle, +#if !ENABLE_TRANSFORM_V1 + TransformType = m_TransformHandle, + PostTransformScaleType = m_PostTransformScaleType, +#else TranslationType = m_TranslationHandle, RotationType = m_RotationHandle, NonUniformScaleType = m_NonUniformScaleHandle, ScaleType = m_ScaleHandle, CompositeScaleType = m_CompositeScaleHandle, +#endif SwitchPredictionSmoothingType = m_SwitchPredictionSmoothingHandle, LocalToWorldType = m_LocalToWorldHandle, DeltaTime = deltaTime, @@ -119,11 +147,16 @@ public void OnUpdate(ref SystemState state) struct SwitchPredictionSmoothingJob : IJobChunk { [ReadOnly] public EntityTypeHandle EntityType; +#if !ENABLE_TRANSFORM_V1 + [ReadOnly] public ComponentTypeHandle TransformType; + [ReadOnly] public ComponentTypeHandle PostTransformScaleType; +#else [ReadOnly] public ComponentTypeHandle TranslationType; [ReadOnly] public ComponentTypeHandle RotationType; [ReadOnly] public ComponentTypeHandle NonUniformScaleType; [ReadOnly] public ComponentTypeHandle ScaleType; [ReadOnly] public ComponentTypeHandle CompositeScaleType; +#endif public ComponentTypeHandle SwitchPredictionSmoothingType; public ComponentTypeHandle LocalToWorldType; public float DeltaTime; @@ -134,31 +167,47 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE { Assert.IsFalse(useEnabledMask); - var hasNonUniformScale = chunk.Has(NonUniformScaleType); - var hasScale = chunk.Has(ScaleType); - var hasAnyScale = hasNonUniformScale || hasScale || chunk.Has(CompositeScaleType); - - NativeArray positions = chunk.GetNativeArray(TranslationType); - NativeArray orientations = chunk.GetNativeArray(RotationType); - NativeArray nonUniformScales = chunk.GetNativeArray(NonUniformScaleType); - NativeArray scales = chunk.GetNativeArray(ScaleType); - NativeArray compositeScales = chunk.GetNativeArray(CompositeScaleType); - NativeArray switchPredictionSmoothings = chunk.GetNativeArray(SwitchPredictionSmoothingType); - NativeArray localToWorlds = chunk.GetNativeArray(LocalToWorldType); +#if !ENABLE_TRANSFORM_V1 + NativeArray transforms = chunk.GetNativeArray(ref TransformType); + NativeArray postTransformScales = new NativeArray(); + if (chunk.Has(ref PostTransformScaleType)) + postTransformScales = chunk.GetNativeArray(ref PostTransformScaleType); +#else + var hasNonUniformScale = chunk.Has(ref NonUniformScaleType); + var hasScale = chunk.Has(ref ScaleType); + var hasAnyScale = hasNonUniformScale || hasScale || chunk.Has(ref CompositeScaleType); + + NativeArray positions = chunk.GetNativeArray(ref TranslationType); + NativeArray orientations = chunk.GetNativeArray(ref RotationType); + NativeArray nonUniformScales = chunk.GetNativeArray(ref NonUniformScaleType); + NativeArray scales = chunk.GetNativeArray(ref ScaleType); + NativeArray compositeScales = chunk.GetNativeArray(ref CompositeScaleType); +#endif + NativeArray switchPredictionSmoothings = chunk.GetNativeArray(ref SwitchPredictionSmoothingType); + NativeArray localToWorlds = chunk.GetNativeArray(ref LocalToWorldType); NativeArray chunkEntities = chunk.GetNativeArray(EntityType); for (int i = 0, count = chunk.Count; i < count; ++i) { +#if !ENABLE_TRANSFORM_V1 + var currentPosition = transforms[i].Position; + var currentRotation = transforms[i].Rotation; +#else var currentPosition = positions[i].Value; var currentRotation = orientations[i].Value; - +#endif var smoothing = switchPredictionSmoothings[i]; if (smoothing.SkipVersion != AppliedVersion) { if (smoothing.CurrentFactor == 0) { +#if !ENABLE_TRANSFORM_V1 + smoothing.InitialPosition = transforms[i].Position - smoothing.InitialPosition; + smoothing.InitialRotation = math.mul(transforms[i].Rotation, math.inverse(smoothing.InitialRotation)); +#else smoothing.InitialPosition = positions[i].Value - smoothing.InitialPosition; smoothing.InitialRotation = math.mul(orientations[i].Value, math.inverse(smoothing.InitialRotation)); +#endif } smoothing.CurrentFactor = math.saturate(smoothing.CurrentFactor + DeltaTime / smoothing.Duration); @@ -173,6 +222,16 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE } var tr = new float4x4(currentRotation, currentPosition); +#if !ENABLE_TRANSFORM_V1 + if (math.distance(transforms[i].Scale, 1f) > 1e-4f) + { + var scale = float4x4.Scale(new float3(transforms[i].Scale)); + tr = math.mul(tr, scale); + } + //TODO: is there a fast way to check if the postTransformScale is the identity? + if(postTransformScales.IsCreated) + tr = math.mul(tr, new float4x4(postTransformScales[i].Value, float3.zero)); +#else if (hasAnyScale) { var scale = hasNonUniformScale @@ -182,7 +241,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE : compositeScales[i].Value; tr = math.mul(tr, scale); } - +#endif localToWorlds[i] = new LocalToWorld { Value = tr }; } } diff --git a/Runtime/SourceGenerators/NetCodeSourceGenerator.dll b/Runtime/SourceGenerators/NetCodeSourceGenerator.dll index 5b67661..f079dea 100644 Binary files a/Runtime/SourceGenerators/NetCodeSourceGenerator.dll and b/Runtime/SourceGenerators/NetCodeSourceGenerator.dll differ diff --git a/Runtime/SourceGenerators/NetCodeSourceGenerator.pdb b/Runtime/SourceGenerators/NetCodeSourceGenerator.pdb index 0812939..e89139a 100644 Binary files a/Runtime/SourceGenerators/NetCodeSourceGenerator.pdb and b/Runtime/SourceGenerators/NetCodeSourceGenerator.pdb differ diff --git a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/CodeGenerator.cs b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/CodeGenerator.cs index 54f017d..3693325 100644 --- a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/CodeGenerator.cs +++ b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/CodeGenerator.cs @@ -78,7 +78,7 @@ static bool CanGenerateType(TypeInformation typeInfo, Context context) public static void GenerateRegistrationSystem(Context context) { //There is nothing to generate in that case. Skip creating an empty system - if(context.generatedTypes.Count == 0 && context.emptyVariantTypes.Count == 0) + if(context.generatedTypes.Count == 0 && context.serializationStrategies.Count == 0) return; using (new Profiler.Auto("GenerateRegistrationSystem")) @@ -93,19 +93,61 @@ public static void GenerateRegistrationSystem(Context context) replacements["GHOST_NAME"] = t; registrationSystemCodeGen.GenerateFragment("GHOST_COMPONENT_LIST", replacements); } - foreach (var t in context.emptyVariantTypes) + + int selfIndex = 0; + foreach (var ss in context.serializationStrategies) { - if(t.Hash == "0") - context.diagnostic.LogError($"Setting invalid hash on variantType {t.VariantType} to {t.Hash}!"); - - replacements["VARIANT_TYPE"] = t.VariantType; - replacements["GHOST_COMPONENT_TYPE"] = t.ComponentType; - replacements["GHOST_VARIANT_HASH"] = t.Hash; - if (t.GhostAttribute != null) - replacements["GHOST_PREFAB_TYPE"] = $"GhostPrefabType.{t.GhostAttribute.PrefabType.ToString()}"; + var typeInfo = ss.TypeInfo; + + if (typeInfo == null) + throw new InvalidOperationException("Must define TypeInfo when using `serializationStrategies.Add`!"); + + if(ss.Hash == "0") + context.diagnostic.LogError($"Setting invalid hash on variantType {ss.VariantTypeName} to {ss.Hash}!"); + + var displayName = ss.DisplayName ?? ss.VariantTypeName; + displayName = SmartTruncateDisplayName(displayName); + + var isDefaultSerializer = string.IsNullOrWhiteSpace(ss.VariantTypeName) || ss.VariantTypeName == ss.ComponentTypeName; + + replacements["VARIANT_TYPE"] = ss.VariantTypeName; + replacements["GHOST_COMPONENT_TYPE"] = ss.ComponentTypeName; + replacements["GHOST_VARIANT_DISPLAY_NAME"] = displayName; + replacements["GHOST_VARIANT_HASH"] = ss.Hash; + replacements["SELF_INDEX"] = selfIndex++.ToString(); + replacements["VARIANT_IS_SERIALIZED"] = ss.IsSerialized ? "1" : "0"; + replacements["GHOST_IS_DEFAULT_SERIALIZER"] = isDefaultSerializer ? "1" : "0"; + replacements["GHOST_SEND_CHILD_ENTITY"] = typeInfo.GhostAttribute != null && typeInfo.GhostAttribute.SendDataForChildEntity ? "1" : "0"; + replacements["TYPE_IS_INPUT_COMPONENT"] = typeInfo.ComponentType == ComponentType.Input ? "1" : "0"; + replacements["TYPE_IS_INPUT_BUFFER"] = typeInfo.ComponentType == ComponentType.CommandData ? "1" : "0"; + replacements["TYPE_IS_TEST_VARIANT"] = typeInfo.IsTestVariant ? "1" : "0"; + replacements["TYPE_HAS_DONT_SUPPORT_PREFAB_OVERRIDES_ATTRIBUTE"] = typeInfo.HasDontSupportPrefabOverridesAttribute ? "1" : "0"; + replacements["TYPE_HAS_SUPPORTS_PREFAB_OVERRIDES_ATTRIBUTE"] = typeInfo.HasSupportsPrefabOverridesAttribute ? "1" : "0"; + replacements["GHOST_PREFAB_TYPE"] = ss.GhostAttribute != null ? $"GhostPrefabType.{ss.GhostAttribute.PrefabType.ToString()}" : "GhostPrefabType.All"; + + if (typeInfo.GhostAttribute != null) + { + if ((typeInfo.GhostAttribute.PrefabType & GhostPrefabType.Client) == GhostPrefabType.InterpolatedClient) + replacements["GHOST_SEND_MASK"] = "GhostSendType.OnlyInterpolatedClients"; + else if ((typeInfo.GhostAttribute.PrefabType & GhostPrefabType.Client) == GhostPrefabType.PredictedClient) + replacements["GHOST_SEND_MASK"] = "GhostSendType.OnlyPredictedClients"; + else if (typeInfo.GhostAttribute.PrefabType == GhostPrefabType.Server) + replacements["GHOST_SEND_MASK"] = "GhostSendType.DontSend"; + else if (typeInfo.GhostAttribute.SendTypeOptimization == GhostSendType.OnlyInterpolatedClients) + replacements["GHOST_SEND_MASK"] = "GhostSendType.OnlyInterpolatedClients"; + else if (typeInfo.GhostAttribute.SendTypeOptimization == GhostSendType.OnlyPredictedClients) + replacements["GHOST_SEND_MASK"] = "GhostSendType.OnlyPredictedClients"; + else if (typeInfo.GhostAttribute.SendTypeOptimization == GhostSendType.AllClients) + replacements["GHOST_SEND_MASK"] = "GhostSendType.AllClients"; + else + replacements["GHOST_SEND_MASK"] = "GhostComponentSerializer.SendMask.DontSend"; + } else - replacements["GHOST_PREFAB_TYPE"] = "GhostPrefabType.All"; - registrationSystemCodeGen.GenerateFragment("GHOST_EMPTY_VARIANT_LIST", replacements); + { + replacements["GHOST_SEND_MASK"] = "GhostSendType.AllClients"; + } + + registrationSystemCodeGen.GenerateFragment("GHOST_SERIALIZATION_STRATEGY_LIST", replacements); } replacements.Clear(); @@ -114,8 +156,27 @@ public static void GenerateRegistrationSystem(Context context) replacements.Clear(); replacements.Add("GHOST_NAMESPACE", context.generatedNs); - registrationSystemCodeGen.GenerateFile("GhostComponentSerializerCollection.cs", string.Empty,replacements, context.batch); + registrationSystemCodeGen.GenerateFile("GhostComponentSerializerCollection.cs", string.Empty, replacements, context.batch); + } + } + + /// Long display names like "Some.Very.Long.Namespace.WithAMassiveStructNameAtTheEnd" will be truncated from the back. + /// E.g. Removing "Some", then "Very" etc. It must fit into the FixedString capacity, otherwise we'll get runtime exceptions during Registration. + static string SmartTruncateDisplayName(string displayName) + { + int indexOf = 0; + const int fixedString64BytesCapacity = 61; + while (displayName.Length - indexOf > fixedString64BytesCapacity && indexOf < displayName.Length) + { + int newIndexOf = displayName.IndexOf('.', indexOf); + if (newIndexOf < 0) newIndexOf = displayName.IndexOf(',', indexOf); + + // We may have to just truncate in the middle of a word. + if (newIndexOf < 0 || newIndexOf >= displayName.Length - 1) + indexOf = Math.Max(0, displayName.Length - fixedString64BytesCapacity); + else indexOf = newIndexOf + 1; } + return displayName.Substring(indexOf, displayName.Length - indexOf); } public static void GenerateGhost(Context context, TypeInformation typeTree) @@ -131,7 +192,7 @@ public static void GenerateGhost(Context context, TypeInformation typeTree) } } - public static void GenerateCommand(Context context, TypeInformation typeTree, CommandSerializer.Type commandType) + public static void GenerateCommand(Context context, TypeInformation typeInfo, CommandSerializer.Type commandType) { void BuildGenerator(Context ctx, TypeInformation typeInfo, CommandSerializer parentGenerator) { @@ -153,7 +214,7 @@ void BuildGenerator(Context ctx, TypeInformation typeInfo, CommandSerializer par return; } } - foreach (var field in typeInfo.Fields) + foreach (var field in typeInfo.GhostFields) BuildGenerator(ctx, field, fieldGen); fieldGen.AppendTarget(parentGenerator); } @@ -161,21 +222,21 @@ void BuildGenerator(Context ctx, TypeInformation typeInfo, CommandSerializer par using(new Profiler.Auto("CodeGen")) { var serializeGenerator = new CommandSerializer(context, commandType); - BuildGenerator(context, typeTree, serializeGenerator); - serializeGenerator.GenerateSerializer(context, typeTree); + BuildGenerator(context, typeInfo, serializeGenerator); + serializeGenerator.GenerateSerializer(context, typeInfo); if (commandType == Generators.CommandSerializer.Type.Input) { // The input component needs to be registered as an empty type variant so that the // ghost component attributes placed on it can be parsed during ghost conversion - var inputGhostAttributes = ComponentFactory.TryGetGhostComponent(typeTree.Symbol); + var inputGhostAttributes = ComponentFactory.TryGetGhostComponent(typeInfo.Symbol); if (inputGhostAttributes == null) inputGhostAttributes = new GhostComponentAttribute(); - var variantHash = Helpers.ComputeVariantHash(typeTree.Symbol, typeTree.Symbol); - context.emptyVariantTypeInfo.Add(typeTree); - context.emptyVariantTypes.Add(new CodeGenerator.Context.EmptyVariant + var variantHash = Helpers.ComputeVariantHash(typeInfo.Symbol, typeInfo.Symbol); + context.serializationStrategies.Add(new CodeGenerator.Context.SerializationStrategyCodeGen { - VariantType = typeTree.TypeFullName.Replace("+", "."), - ComponentType = typeTree.TypeFullName.Replace("+", "."), + TypeInfo = typeInfo, + VariantTypeName = typeInfo.TypeFullName.Replace("+", "."), + ComponentTypeName = typeInfo.TypeFullName.Replace("+", "."), Hash = variantHash.ToString(), GhostAttribute = inputGhostAttributes }); @@ -185,7 +246,7 @@ void BuildGenerator(Context ctx, TypeInformation typeInfo, CommandSerializer par string bufferName; using (new Profiler.Auto("GenerateInputBufferType")) { - if (!GenerateInputBufferType(context, typeTree, out bufferTypeTree, + if (!GenerateInputBufferType(context, typeInfo, out bufferTypeTree, out bufferSymbol, out bufferName)) return; } @@ -201,126 +262,52 @@ void BuildGenerator(Context ctx, TypeInformation typeInfo, CommandSerializer par { // Check if the input type has any GhostField attributes, needs to first // lookup the symbol from the candidates list and get the field members from there - bool hasGhostField = false; - foreach (var member in typeTree.Symbol.GetMembers()) + bool hasGhostFields = false; + foreach (var member in typeInfo.Symbol.GetMembers()) { foreach (var attribute in member.GetAttributes()) { if (attribute.AttributeClass != null && attribute.AttributeClass.Name is "GhostFieldAttribute" or "GhostField") - hasGhostField = true; + hasGhostFields = true; } } - // Parse the generated input buffer as a component so it will be included in snapshot replication + + // Parse the generated input buffer as a component so it will be included in snapshot replication. // This only needs to be done if the input struct has ghost fields inside as the generated input - // buffer should then be replicated to remote players - if (hasGhostField) + // buffer should then be replicated to remote players. + if (hasGhostFields) // Ignore GhostEnabledBit here as inputs cannot have them. { - GenerateInputBufferGhostComponent(context, typeTree, bufferName, bufferSymbol); + GenerateInputBufferGhostComponent(context, typeInfo, bufferName, bufferSymbol); } else { - // If there are no ghost fields we need to add the buffer to the empty variant - // list to save the ghost component attributes + // We must add the serialization strategy even if there are no ghost fields, as empty variants + // still save the ghost component attributes. var bufferVariantHash = Helpers.ComputeVariantHash(bufferTypeTree.Symbol, bufferTypeTree.Symbol); - context.emptyVariantTypeInfo.Add(typeTree); - context.emptyVariantTypes.Add(new CodeGenerator.Context.EmptyVariant + context.diagnostic.LogInfo($"Adding SerializationStrategy for input buffer {bufferTypeTree.TypeFullName}, which doesn't have any GhostFields, as we still need to store the GhostComponentAttribute data."); + context.serializationStrategies.Add(new CodeGenerator.Context.SerializationStrategyCodeGen { - VariantType = bufferTypeTree.TypeFullName.Replace("+", "."), - ComponentType = bufferTypeTree.TypeFullName.Replace("+", "."), + TypeInfo = typeInfo, + IsSerialized = false, + VariantTypeName = bufferTypeTree.TypeFullName.Replace("+", "."), + ComponentTypeName = bufferTypeTree.TypeFullName.Replace("+", "."), Hash = bufferVariantHash.ToString(), GhostAttribute = inputGhostAttributes }); - } } } } } - public static void GenerateMetaData(Context context, TypeInformation[] allTypes) - { - using (new Profiler.Auto("GenerateMetaDataRegistrationSystem")) - { - context.ResetState(); - var assemblyNameSanitized = context.executionContext.Compilation.Assembly.Name.Replace(".", "").Replace("+", "_").Replace("-", "_"); - context.generatorName = $"NetCodeTypeMetaDataRegistrationSystem_{assemblyNameSanitized}"; - context.diagnostic.LogInfo($"Begun generation of '{context.generatorName}', checking {allTypes.Length} types."); - - var metaDataRegistrationSystemCodeGen = context.codeGenCache.GetTemplate(MetaDataRegistrationSystem); - metaDataRegistrationSystemCodeGen = metaDataRegistrationSystemCodeGen.Clone(); - - var replacements = new Dictionary(8); - replacements["REGISTRATION_SYSTEM_FILE_NAME"] = context.generatorName; - - var alreadyAddedNamespaces = new HashSet - { - // Rule out the erroneous ones. - string.Empty, - null, - " ", - }; - - const string unityCodeGenNamespace = "Unity.NetCode.Generated"; - replacements["GHOST_NAMESPACE"] = unityCodeGenNamespace; - - int numTypesAdded = 0; - foreach (var ns in context.imports) - { - var validNamespaceForType = GetValidNamespaceForType(context.generatedNs, ns); - if (!alreadyAddedNamespaces.Contains(validNamespaceForType)) - { - alreadyAddedNamespaces.Add(validNamespaceForType); - replacements["GHOST_USING"] = validNamespaceForType; - metaDataRegistrationSystemCodeGen.GenerateFragment("GHOST_USING_STATEMENT", replacements); - } - } - - foreach (var typeInfo in allTypes) - { - context.executionContext.CancellationToken.ThrowIfCancellationRequested(); - - if (!typeInfo.IsValid) - { - context.diagnostic.LogInfo($"NOT generating meta-data for ${typeInfo.TypeFullName} as not a valid NetCode type."); - continue; - } - context.diagnostic.LogInfo($"Generating meta-data for ${typeInfo.TypeFullName}"); - - if (!alreadyAddedNamespaces.Contains(typeInfo.Namespace)) - { - alreadyAddedNamespaces.Add(typeInfo.Namespace); - replacements["GHOST_USING"] = typeInfo.Namespace; - metaDataRegistrationSystemCodeGen.GenerateFragment("GHOST_USING_STATEMENT", replacements); - } - - // If this is a variant, we need to parse out the type it's for, and ensure we use that in the hash below. - var variantTypeFullName = typeInfo.TypeFullName.Replace("+", "."); - var componentTypeFullName = typeInfo.Symbol.GetFullTypeName().Replace("+", "."); - - replacements["VARIANT_TYPE_HASH"] = Helpers.ComputeVariantHash(variantTypeFullName, componentTypeFullName).ToString(); - replacements["TYPE_IS_INPUT_COMPONENT"] = typeInfo.ComponentType == ComponentType.Input ? "true" : "false"; - replacements["TYPE_IS_INPUT_BUFFER"] = typeInfo.ComponentType == ComponentType.CommandData ? "true" : "false"; - replacements["TYPE_IS_TEST_VARIANT"] = typeInfo.IsTestVariant ? "true" : "false"; - replacements["TYPE_HAS_DONT_SUPPORT_PREFAB_OVERRIDES_ATTRIBUTE"] = typeInfo.HasDontSupportPrefabOverridesAttribute ? "true" : "false"; - replacements["TYPE_HAS_SUPPORTS_PREFAB_OVERRIDES_ATTRIBUTE"] = typeInfo.HasSupportsPrefabOverridesAttribute ? "true" : "false"; - metaDataRegistrationSystemCodeGen.GenerateFragment("GHOST_META_DATA_LIST", replacements); - numTypesAdded++; - } - - context.diagnostic.LogInfo($"Completed generation of meta-data registration system, {numTypesAdded} of {allTypes.Length} types total."); - - if(numTypesAdded > 0) - metaDataRegistrationSystemCodeGen.GenerateFile(context.generatorName + ".cs", unityCodeGenNamespace, replacements, context.batch); - else context.diagnostic.LogInfo("Meta-data registration file will not be created as no types to register!"); - } - } - #region Internal for Code Generation private static bool GenerateInputBufferType(Context context, TypeInformation typeTree, out TypeInformation bufferTypeTree, out ITypeSymbol bufferSymbol, out string bufferName) { + // TODO - Code gen should handle throwing an exception for a zero-sized buffer with [GhostEnabledBit]. + // Add the generated code for the command type to the compilation syntax tree and // fetch its symbol for further processing var nameAndSource = context.batch[context.batch.Count - 1]; @@ -346,7 +333,7 @@ private static bool GenerateInputBufferType(Context context, TypeInformation typ bufferTypeTree = typeBuilder.BuildTypeInformation(bufferSymbol, null); if (bufferTypeTree == null) { - context.diagnostic.LogError($"Failed to generate type information for symbol ${bufferSymbol.ToDisplayString()}"); + context.diagnostic.LogError($"Failed to generate type information for symbol ${bufferSymbol.ToDisplayString()}!"); return false; } context.types.Add(bufferTypeTree); @@ -365,7 +352,10 @@ private static void GenerateInputBufferGhostComponent(Context context, TypeInfor context.ResetState(); var bufferTypeTree = typeBuilder.BuildTypeInformation(bufferSymbol, null, ghostFieldOverride); if (bufferTypeTree == null) + { + context.diagnostic.LogError($"Failed to generate type information for symbol ${bufferSymbol.ToDisplayString()}!"); return; + } // Set ghost component attribute from values set on the input component source, or defaults // if not present, except for the OwnerSendType which can only be SendToNonOwner since it's // a dynamic buffer @@ -383,8 +373,19 @@ private static void GenerateInputBufferGhostComponent(Context context, TypeInfor else bufferTypeTree.GhostAttribute = new GhostComponentAttribute { OwnerSendType = SendToOwnerType.SendToNonOwner }; + var variantHash = Helpers.ComputeVariantHash(bufferTypeTree.Symbol, bufferTypeTree.Symbol); + context.serializationStrategies.Add(new CodeGenerator.Context.SerializationStrategyCodeGen + { + TypeInfo = bufferTypeTree, + VariantTypeName = bufferTypeTree.TypeFullName.Replace("+", "."), + ComponentTypeName = bufferTypeTree.TypeFullName.Replace("+", "."), + Hash = variantHash.ToString(), + GhostAttribute = bufferTypeTree.GhostAttribute, + IsSerialized = true, + }); + context.types.Add(bufferTypeTree); - context.diagnostic.LogInfo($"Generating ghost for {bufferTypeTree.TypeFullName}"); + context.diagnostic.LogInfo($"Generating ghost for input buffer {bufferTypeTree.TypeFullName}"); GenerateGhost(context, bufferTypeTree); } @@ -441,7 +442,7 @@ private static ComponentSerializer InternalGenerateType(Context context, TypeInf bool composite = type.Attribute.composite; int index = 0; - foreach (var field in type.Fields) + foreach (var field in type.GhostFields) { var generator = InternalGenerateType(context, field, $"{field.DeclaringTypeFullName}.{field.FieldName}"); //Type not found. (error should be already logged. @@ -454,7 +455,7 @@ private static ComponentSerializer InternalGenerateType(Context context, TypeInf var overrides = generator.GenerateCompositeOverrides(context, field.Parent); if (overrides != null) generator.AppendTarget(typeGenerator); - foreach (var f in generator.TypeInformation.Fields) + foreach (var f in generator.TypeInformation.GhostFields) { var g = InternalGenerateType(context, f, $"{f.DeclaringTypeFullName}.{f.FieldName}"); g?.GenerateFields(context, f.Parent, overrides); @@ -478,8 +479,8 @@ private static ComponentSerializer InternalGenerateType(Context context, TypeInf } } - if (type.Fields.Count == 0) - context.diagnostic.LogError($"Couldn't find the TypeDescriptor for the type {type.Description} when processing {fullFieldName}", type.Location); + if (type.GhostFields.Count == 0 && !type.ShouldSerializeEnabledBit) + context.diagnostic.LogError($"Couldn't find the TypeDescriptor for the type {type.Description} when processing {fullFieldName}! Types must have either valid [GhostField] attributes, or a [GhostEnabledBit] (on an IEnableableComponent).", type.Location); if (composite) { @@ -561,15 +562,18 @@ public class Context public List types; public HashSet imports; public HashSet generatedTypes; - public struct EmptyVariant + public struct SerializationStrategyCodeGen { - public string ComponentType; - public string VariantType; + public TypeInformation TypeInfo; + public string DisplayName; + public string ComponentTypeName; + public string VariantTypeName; public string Hash; + public bool IsSerialized; public GhostComponentAttribute GhostAttribute; + } - public List emptyVariantTypeInfo; - public HashSet emptyVariantTypes; + public List serializationStrategies; public string variantType; public ulong variantHash; public string generatorName; @@ -578,14 +582,15 @@ public struct CurrentFieldState { public int numFields; public int curChangeMask; - public ulong ghostfieldHash; + public ulong ghostFieldHash; } public CurrentFieldState FieldState; + public void ResetState() { FieldState.numFields = 0; FieldState.curChangeMask = 0; - FieldState.ghostfieldHash = 0; + FieldState.ghostFieldHash = 0; variantType = null; variantHash = 0; imports.Clear(); @@ -606,12 +611,11 @@ string GenerateNamespaceFromAssemblyName(string assemblyName) { executionContext = context; types = new List(16); - emptyVariantTypeInfo = new List(16); + serializationStrategies = new List(32); codeGenCache = new CodeGenCache(templateFileProvider, reporter); batch = new List(256); imports = new HashSet(); generatedTypes = new HashSet(); - emptyVariantTypes = new HashSet(); diagnostic = reporter; generatedNs = GenerateNamespaceFromAssemblyName(assemblyName); registry = typeRegistry; diff --git a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/TypeInformation.cs b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/TypeInformation.cs index b5da59f..66a378e 100644 --- a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/TypeInformation.cs +++ b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/CodeGenerator/TypeInformation.cs @@ -74,7 +74,8 @@ internal class TypeInformation //The syntax tree and text span location of the type public Location Location; - public List Fields = new List(); + public List GhostFields = new List(); + public bool ShouldSerializeEnabledBit; public bool HasDontSupportPrefabOverridesAttribute; public bool HasSupportsPrefabOverridesAttribute; public bool IsTestVariant; diff --git a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/DefaultTypes.cs b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/DefaultTypes.cs index 2601665..5c22cad 100644 --- a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/DefaultTypes.cs +++ b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/DefaultTypes.cs @@ -147,6 +147,42 @@ class DefaultTypes Template = "NetCode.GhostSnapshotValueFloatUnquantized.cs" }, new TypeRegistryEntry + { + Type = "System.Double", + Quantized = true, + Smoothing = SmoothingAction.Interpolate, + SupportCommand = false, + Composite = false, + Template = "NetCode.GhostSnapshotValueDouble.cs" + }, + new TypeRegistryEntry + { + Type = "System.Double", + Quantized = true, + Smoothing = SmoothingAction.Clamp, + SupportCommand = false, + Composite = false, + Template = "NetCode.GhostSnapshotValueDouble.cs" + }, + new TypeRegistryEntry + { + Type = "System.Double", + Quantized = false, + Smoothing = SmoothingAction.Clamp, + SupportCommand = true, + Composite = false, + Template = "NetCode.GhostSnapshotValueDoubleUnquantized.cs" + }, + new TypeRegistryEntry + { + Type = "System.Double", + Quantized = false, + Smoothing = SmoothingAction.Interpolate, + SupportCommand = false, + Composite = false, + Template = "NetCode.GhostSnapshotValueDoubleUnquantized.cs" + }, + new TypeRegistryEntry { Type = "Unity.Mathematics.float2", Quantized = true, diff --git a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Generators/Factories/RpcFactory.cs b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Generators/Factories/RpcFactory.cs index 4636600..96879db 100644 --- a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Generators/Factories/RpcFactory.cs +++ b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Generators/Factories/RpcFactory.cs @@ -31,7 +31,7 @@ public static void Generate(IReadOnlyList rpcCandidates, CodeGenerat // If the serializer type already exist we can just skip generation if (codeGenContext.executionContext.Compilation.GetSymbolsWithName(GetRpcSerializerName(candidateSymbol)).FirstOrDefault() != null) { - codeGenContext.diagnostic.LogInfo($"Skipping code-gen for {candidateSymbol.Name} because a serializer for it already exists"); + codeGenContext.diagnostic.LogInfo($"Skipping code-gen for {candidateSymbol.Name} because an rpc serializer for it already exists"); continue; } @@ -40,6 +40,7 @@ public static void Generate(IReadOnlyList rpcCandidates, CodeGenerat var typeInfo = typeBuilder.BuildTypeInformation(candidateSymbol, null); if (typeInfo == null) continue; + codeGenContext.types.Add(typeInfo); codeGenContext.diagnostic.LogInfo($"Generating rpc for ${typeInfo.TypeFullName}"); CodeGenerator.GenerateCommand(codeGenContext, typeInfo, CommandSerializer.Type.Rpc); diff --git a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/RoslynExtensions.cs b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/RoslynExtensions.cs index b83a490..3a2c8f2 100644 --- a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/RoslynExtensions.cs +++ b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/RoslynExtensions.cs @@ -332,6 +332,11 @@ public static string GetFullyQualifiedNamespace(ISymbol symbol) return symbol.ContainingNamespace.ToDisplayString(); } + /// + /// This is the only trustful (i.e. proper) way to check for interfaces. + /// This is also the reason why we can't rely on the SyntaxTreeVisitor for retrieving the candidates, + /// and why we need to collect pretty much all the structs with at least one interface. + /// public static AttributeData GetAttribute(ISymbol symbol, string attributeNamespace, string attributeName) { using (new Profiler.Auto("GetAttribute")) diff --git a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/SourceGeneratorHelpers.cs b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/SourceGeneratorHelpers.cs index 40d6853..dd01b61 100644 --- a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/SourceGeneratorHelpers.cs +++ b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/Helpers/SourceGeneratorHelpers.cs @@ -83,7 +83,7 @@ static public void SetupContext(GeneratorExecutionContext executionContext) IsDotsRuntime = executionContext.ParseOptions.PreprocessorSymbolNames.Contains("UNITY_DOTSRUNTIME"); IsUnity2021_OrNewer = executionContext.ParseOptions.PreprocessorSymbolNames.Any(d => d == "UNITY_2022_1_OR_NEWER" || d == "UNITY_2021_3_OR_NEWER"); //Setup the current project folder directory by inspecting the context for global options or additional files, depending on the current Unity version - if (!IsUnity2021_OrNewer) + if (!IsUnity2021_OrNewer || IsDotsRuntime) { SupportTemplateFromAdditionalFiles = false; if (executionContext.AdditionalFiles.Any() && !string.IsNullOrEmpty(executionContext.AdditionalFiles[0].Path)) diff --git a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/NetCodeSourceGenerator.csproj b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/NetCodeSourceGenerator.csproj index 3201eb3..c226125 100644 --- a/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/NetCodeSourceGenerator.csproj +++ b/Runtime/SourceGenerators/Source~/NetCodeSourceGenerator/NetCodeSourceGenerator.csproj @@ -9,7 +9,6 @@ - @@ -34,5 +33,7 @@ + + \ No newline at end of file diff --git a/Runtime/SourceGenerators/Source~/Tests/SourceGeneratorTests.cs b/Runtime/SourceGenerators/Source~/Tests/SourceGeneratorTests.cs index 52db015..e0d6684 100644 --- a/Runtime/SourceGenerators/Source~/Tests/SourceGeneratorTests.cs +++ b/Runtime/SourceGenerators/Source~/Tests/SourceGeneratorTests.cs @@ -11,6 +11,9 @@ namespace Unity.NetCode.GeneratorTests { + // TODO: Add tests for GhostEnabledBits. + // TODO: Add tests for types moved to SerializationStrategy. + [TestFixture] class SourceGeneratorTests : BaseTest { @@ -100,7 +103,7 @@ public void SourceGenerator_PrimitiveTypes() Assert.AreEqual(1, walker.receiver.Candidates.Count); //Check generated files match var resuls = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(3, resuls.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, resuls.GeneratedSources.Length, "Num generated files does not match"); var outputTree = resuls.GeneratedSources[0].SyntaxTree; var snapshotDataSyntax = outputTree.GetRoot().DescendantNodes().OfType() .FirstOrDefault(node => node.Identifier.ValueText == "Snapshot"); @@ -206,7 +209,7 @@ public void SourceGenerator_Mathematics() //Check generated files match var resuls = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(3, resuls.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, resuls.GeneratedSources.Length, "Num generated files does not match"); var outputTree = resuls.GeneratedSources[0].SyntaxTree; var snapshotDataSyntax = outputTree.GetRoot().DescendantNodes().OfType() .FirstOrDefault(node => node.Identifier.ValueText == "Snapshot"); @@ -305,7 +308,7 @@ public struct InnerComponent : IComponentData Assert.AreEqual(1, walker.receiver.Candidates.Count); var resuls = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(3, resuls.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, resuls.GeneratedSources.Length, "Num generated files does not match"); var outputTree = resuls.GeneratedSources[0].SyntaxTree; var snapshotDataSyntax = outputTree.GetRoot().DescendantNodes().OfType() .FirstOrDefault(node => node.Identifier.ValueText == "Snapshot"); @@ -345,7 +348,7 @@ public void SourceGenerator_FlatType() var results = GeneratorTestHelpers.RunGenerators(tree); Assert.AreEqual(0, results.Diagnostics.Count(d=>d.Severity == DiagnosticSeverity.Error)); - Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); } [Test] @@ -382,7 +385,7 @@ public struct ProblematicType : IComponentData var resuls = GeneratorTestHelpers.RunGenerators(tree); Assert.AreEqual(0, resuls.Diagnostics.Count(m=>m.Severity == DiagnosticSeverity.Error)); - Assert.AreEqual(3, resuls.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, resuls.GeneratedSources.Length, "Num generated files does not match"); } [Test] @@ -430,7 +433,7 @@ public struct RotationVariant Assert.AreEqual(1, diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error), "errorCount"); Assert.AreEqual("InvalidRotation: Cannot find member Value type: float3 in Rotation", diagnostics.First(d => d.Severity == DiagnosticSeverity.Error).GetMessage()); - Assert.AreEqual(4, resuls.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(3, resuls.GeneratedSources.Length, "Num generated files does not match"); var outputTree = resuls.GeneratedSources[0].SyntaxTree; var snapshotDataSyntax = outputTree.GetRoot().DescendantNodes().OfType() @@ -516,7 +519,7 @@ public struct InvalidVariant //All the variants are detected as candidates Assert.AreEqual(2, walker.receiver.Variants.Count); var results = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); var diagnostics = results.Diagnostics; //Expect to see one error Assert.AreEqual(1, diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error)); @@ -570,7 +573,7 @@ public struct CommandTest : ICommandData tree.GetCompilationUnitRoot().Accept(walker); Assert.AreEqual(1, walker.receiver.Candidates.Count); var results = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(4, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); var diagnostics = results.Diagnostics; //Expect to see one error if (diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error) != 0) @@ -631,7 +634,7 @@ public struct CommandData : ICommandData Assert.AreEqual(2, walker.receiver.Candidates.Count); var results = GeneratorTestHelpers.RunGenerators(tree); //only the command serializer - Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(1, results.GeneratedSources.Length, "Num generated files does not match"); //But some errors are reported too var diagnostics = results.Diagnostics.Where(m=>m.Severity == DiagnosticSeverity.Error).ToArray(); Assert.AreEqual(3, diagnostics.Length); @@ -741,7 +744,7 @@ public struct MyType : IComponentData //Check generated files match var templateTree = CSharpSyntaxTree.ParseText(customTemplates); var results = GeneratorTestHelpers.RunGenerators(tree, templateTree); - Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); var outputTree = results.GeneratedSources[0].SyntaxTree; var snapshotDataSyntax = outputTree.GetRoot().DescendantNodes().OfType() @@ -794,8 +797,8 @@ public struct MyBuffer : IBufferElementData // No error during processing Assert.AreEqual(0, results.Diagnostics.Count(m => m.Severity == DiagnosticSeverity.Error)); // No ghost snapshot serializer is generated (but does contain serializer collection with empty variants + client-to-server command serializer) - Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); - Assert.IsTrue(results.GeneratedSources[0].SourceText.ToString().Contains("AddEmptyVariant")); + Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.IsTrue(results.GeneratedSources[0].SourceText.ToString().Contains("SerializerIndex = -1")); Assert.AreEqual(false, results.GeneratedSources[1].SyntaxTree.ToString().Contains("GhostComponentSerializer.State")); } @@ -883,7 +886,7 @@ public struct MyType : IComponentData tree = CSharpSyntaxTree.ParseText(testDataCorrect); templateTree = CSharpSyntaxTree.ParseText(customTemplates); results = GeneratorTestHelpers.RunGenerators(tree, templateTree); - Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); var outputTree = results.GeneratedSources[0].SyntaxTree; var snapshotDataSyntax = outputTree.GetRoot().DescendantNodes().OfType() .FirstOrDefault(node => node.Identifier.ValueText == "Snapshot"); @@ -970,7 +973,7 @@ public struct Translation2d var compilation = GeneratorTestHelpers.CreateCompilation(tree, templateTree); var driver = GeneratorTestHelpers.CreateGeneratorDriver().AddAdditionalTexts(additionalTexts); var results = driver.RunGenerators(compilation).GetRunResult(); - Assert.AreEqual(3, results.GeneratedTrees.Length); + Assert.AreEqual(2, results.GeneratedTrees.Length, "Num generated files does not match"); var outputTree = results.GeneratedTrees[0]; var snapshotDataSyntax = outputTree.GetRoot().DescendantNodes().OfType() .FirstOrDefault(node => node.Identifier.ValueText == "Snapshot"); @@ -1005,7 +1008,7 @@ public struct DefaultComponent : IComponentData var results = GeneratorTestHelpers.RunGenerators(tree); //Parse the output and check that the flag on the generated class is correct (one source is registration system) - Assert.AreEqual(3, results.GeneratedSources.Count(), "Num generated files does not match"); + Assert.AreEqual(2, results.GeneratedSources.Count(), "Num generated files does not match"); var outputTree = results.GeneratedSources[0].SyntaxTree; var initBlockWalker = new InializationBlockWalker(); outputTree.GetCompilationUnitRoot().Accept(initBlockWalker); @@ -1024,12 +1027,13 @@ public struct DefaultComponent : IComponentData Assert.IsNotNull(componentTypeAssignmet); Assert.AreEqual(componentTypeAssignmet.Right.ToString(), "SendToOwnerType.All"); + // TODO: Fix this, as it has been moved to the SS. // SendDataForChildEntity = false - componentTypeAssignmet = initBlockWalker.intializer.Expressions.FirstOrDefault(e => - ((AssignmentExpressionSyntax) e).Left.ToString() == "SendForChildEntities") as - AssignmentExpressionSyntax; - Assert.IsNotNull(componentTypeAssignmet); - Assert.AreEqual(componentTypeAssignmet.Right.ToString(), "false"); + // componentTypeAssignmet = initBlockWalker.intializer.Expressions.FirstOrDefault(e => + // ((AssignmentExpressionSyntax) e).Left.ToString() == "SendForChildEntities") as + // AssignmentExpressionSyntax; + // Assert.IsNotNull(componentTypeAssignmet); + // Assert.AreEqual(componentTypeAssignmet.Right.ToString(), "0"); } [Test] @@ -1061,7 +1065,7 @@ public struct DontSendToChild : IComponentData var tree = CSharpSyntaxTree.ParseText(testData); var results = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(5, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(4, results.GeneratedSources.Length, "Num generated files does not match"); var diagnostics = results.Diagnostics; Assert.AreEqual(0, diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error)); //Parse the output and check that the flag on the generated class is correct @@ -1071,11 +1075,13 @@ public struct DontSendToChild : IComponentData var initBlockWalker = new InializationBlockWalker(); outputTree.GetCompilationUnitRoot().Accept(initBlockWalker); Assert.IsNotNull(initBlockWalker.intializer); - var componentTypeAssignmet = initBlockWalker.intializer.Expressions.FirstOrDefault(e => - ((AssignmentExpressionSyntax) e).Left.ToString() == "SendForChildEntities") as - AssignmentExpressionSyntax; - Assert.IsNotNull(componentTypeAssignmet); - Assert.AreEqual(componentTypeAssignmet.Right.ToString(), (i == 1 ? "true" : "false"), "Only the GhostComponent explicitly sending child entities should have that flag."); + + // TODO: Fix this, as it has been moved to the SS. + // var componentTypeAssignmet = initBlockWalker.intializer.Expressions.FirstOrDefault(e => + // ((AssignmentExpressionSyntax) e).Left.ToString() == "SendForChildEntities") as + // AssignmentExpressionSyntax; + // Assert.IsNotNull(componentTypeAssignmet); + // Assert.AreEqual(componentTypeAssignmet.Right.ToString(), (i == 1 ? "1" : "0"), "Only the GhostComponent explicitly sending child entities should have that flag."); } } @@ -1114,7 +1120,7 @@ public struct SendToChild : IComponentData var tree = CSharpSyntaxTree.ParseText(testData); var results = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); var diagnostics = results.Diagnostics; Assert.AreEqual(0, diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error)); //Parse the output and check that the flag on the generated class is correct @@ -1209,7 +1215,7 @@ public struct TestComponent2 : IComponentData var tree = CSharpSyntaxTree.ParseText(testData); var results = GeneratorTestHelpers.RunGenerators(tree); Assert.AreEqual(0, results.Diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error || d.Severity == DiagnosticSeverity.Error)); - Assert.AreEqual(4, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); Assert.IsTrue(results.GeneratedSources[0].SourceText.ToString().Contains("TestComponent")); Assert.IsTrue(results.GeneratedSources[1].SourceText.ToString().Contains("TestComponent2")); @@ -1267,7 +1273,7 @@ public struct TestComponent : IComponentData } } Assert.AreEqual(0, errorCount); - Assert.AreEqual(4, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); var hintA=Generators.Utilities.TypeHash.FNV1A64(Path.Combine(GeneratorTestHelpers.GeneratedAssemblyName, "A_TestComponentSerializer.cs")); var hintB=Generators.Utilities.TypeHash.FNV1A64(Path.Combine(GeneratorTestHelpers.GeneratedAssemblyName, "B_TestComponentSerializer.cs")); var hintG=Generators.Utilities.TypeHash.FNV1A64(Path.Combine(GeneratorTestHelpers.GeneratedAssemblyName, "GhostComponentSerializerCollection.cs")); @@ -1303,7 +1309,7 @@ public struct TestComponent : IComponentData } } Assert.AreEqual(0, errorCount); - Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(2, results.GeneratedSources.Length, "Num generated files does not match"); var expetedHint1=Generators.Utilities.TypeHash.FNV1A64(Path.Combine(GeneratorTestHelpers.GeneratedAssemblyName, "VERYVERYVERYLONG.VERYVERYVERYLONG.VERYVERYVERYLONG.VERYVERYVERYLONG.VERYVERYVERYLONG.VERYVERYVERYLONG.VERYVERYVERYLONG.VERYVERYVERYLONG.VERYVERYVERYLONG_TestComponentSerializer.cs")); Assert.AreEqual($"{expetedHint1}.cs",results.GeneratedSources[0].HintName); @@ -1336,7 +1342,7 @@ public struct PlayerInput : IInputComponentData // Should get input buffer struct (IInputBufferData etc) and the command data (ICommandDataSerializer etc) generated from that // and the registration system with the empty variant registration data var results = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(4, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(3, results.GeneratedSources.Length, "Num generated files does not match"); var bufferSourceData = results.GeneratedSources[0].SyntaxTree; var commandSourceData = results.GeneratedSources[1].SyntaxTree; @@ -1379,7 +1385,7 @@ public struct PlayerInput : IInputComponentData Assert.AreEqual(1, walker.receiver.Candidates.Count); var results = GeneratorTestHelpers.RunGenerators(tree); - Assert.AreEqual(5, results.GeneratedSources.Length, "Num generated files does not match"); + Assert.AreEqual(4, results.GeneratedSources.Length, "Num generated files does not match"); var bufferSourceData = results.GeneratedSources[0].SyntaxTree; var commandSourceData = results.GeneratedSources[1].SyntaxTree; var componentSourceData = results.GeneratedSources[2].SyntaxTree; @@ -1425,11 +1431,10 @@ public struct PlayerInput : IInputComponentData Assert.IsNotNull(maskBits); Assert.AreEqual("4", maskBits.Declaration.Variables[0].Initializer.Value.ToString()); - var registrationSyntax = registrationSourceData.GetRoot().DescendantNodes().OfType() - .FirstOrDefault(node => node.Identifier.ValueText == "GhostComponentSerializerRegistrationSystem"); + var registrationSyntax = registrationSourceData.GetRoot().DescendantNodes().OfType() + .FirstOrDefault(node => node.ToString().Contains("IGhostComponentSerializerRegistration")); Assert.IsNotNull(registrationSyntax); - sourceText = registrationSyntax.GetText(); - Assert.AreEqual(1, sourceText.Lines.Where((line => line.ToString().Contains("AddSerializer(PlayerInputInputBufferDataGhostComponentSerializer.State)"))).Count()); + Assert.AreEqual(1, registrationSourceData.GetText().Lines.Where((line => line.ToString().Contains("data.AddSerializer(PlayerInputInputBufferDataGhostComponentSerializer.State)"))).Count()); } } } diff --git a/Runtime/Stats/netdbg.js b/Runtime/Stats/netdbg.js index 7e1abd4..ba07740 100644 --- a/Runtime/Stats/netdbg.js +++ b/Runtime/Stats/netdbg.js @@ -354,15 +354,7 @@ NetDbg.prototype.createName = function(name) { div.appendChild(document.createTextNode(name)); return div; } -NetDbg.prototype.createPredictionError = function(err) { - var div = document.createElement("div"); - div.style.display = "inline-block"; - div.style.width = "0"; - div.style.whiteSpace = "nowrap"; - div.style.marginLeft = "400px"; - div.appendChild(document.createTextNode("" + err)); - return div; -} + NetDbg.prototype.createCount = function(count, uncompressed) { var div = document.createElement("div"); div.style.display = "inline-block"; @@ -390,6 +382,9 @@ NetDbg.prototype.createInstSize = function(sizeBits, sizeBytes) { div.appendChild(document.createTextNode("" + sizeBits + " (" + sizeBytes + ")")); return div; } +NetDbg.prototype.alternateColorHighlighting = function(element, index) { + element.style.backgroundColor = index % 2 === 0 ? "white" : "#efefef"; +} NetDbg.prototype.select = function(evt) { var offset = evt.clientX; @@ -421,7 +416,9 @@ NetDbg.prototype.select = function(evt) { descr.appendChild(discard); var totalSize = 0; + descr.appendChild(document.createElement("hr")); var headerDiv = document.createElement("div"); + headerDiv.style.fontWeight = "bold"; var nameHead = this.createName("Ghost Type"); headerDiv.appendChild(nameHead); @@ -441,6 +438,7 @@ NetDbg.prototype.select = function(evt) { continue; var sectionDiv = document.createElement("div"); + this.alternateColorHighlighting(sectionDiv, i+1); var name = this.createName(content.names[i]); sectionDiv.appendChild(name); @@ -458,23 +456,45 @@ NetDbg.prototype.select = function(evt) { totalSize += type.size; } if (content.frames[this.selection].predictionError != undefined) { - var titleText = "Prediction errors"; - var titleDiv = document.createElement("div"); - titleDiv.className = "DetailsTitle"; - titleDiv.appendChild(document.createTextNode(titleText)); - descr.appendChild(titleDiv); + + var errorCount = 0; + var table = document.createElement("table"); for (var err = 0; err < content.errors.length; ++err) { if (content.enabledErrors[err]) { - var sectionDiv = document.createElement("div"); - var name = this.createName(content.errors[err]); - sectionDiv.appendChild(name); - var error = this.createPredictionError(content.frames[this.selection].predictionError[err]); - sectionDiv.appendChild(error); - descr.appendChild(sectionDiv); + errorCount++; + var sectionTr = document.createElement("tr"); + this.alternateColorHighlighting(sectionTr, errorCount); + + var nameTd = document.createElement("td"); + nameTd.textContent = "" + content.errors[err]; + nameTd.style.minWidth = "200px"; + nameTd.style.padding = "0px 40px 0px 0px"; + sectionTr.appendChild(nameTd); + + var errorTd = document.createElement("td"); + errorTd.textContent = content.frames[this.selection].predictionError[err]; + nameTd.style.minWidth = "100px"; + errorTd.style.padding = "0px 40px 0px 0px"; + sectionTr.appendChild(errorTd); + + table.appendChild(sectionTr); } } - } + // Only show the Prediction errors if we actually have some. + if(errorCount !== 0) + { + descr.appendChild(document.createElement("hr")); + + var titleDiv = document.createElement("div"); + titleDiv.className = "DetailsTitle"; + titleDiv.style.fontWeight = "bold"; + titleDiv.appendChild(document.createTextNode("Prediction errors")); + descr.appendChild(titleDiv); + + descr.appendChild(table); + } + } var avgCommandAge = 0; var avgTimeScale = 0; @@ -498,6 +518,7 @@ NetDbg.prototype.select = function(evt) { var titleText = "Network frame " + this.selection; var titleDiv = document.createElement("div"); titleDiv.className = "DetailsTitle"; + titleDiv.style.fontWeight = "bold"; titleDiv.appendChild(document.createTextNode(titleText)); div.appendChild(titleDiv); diff --git a/Runtime/Unity.NetCode.asmdef b/Runtime/Unity.NetCode.asmdef index b0513c4..9b161e2 100644 --- a/Runtime/Unity.NetCode.asmdef +++ b/Runtime/Unity.NetCode.asmdef @@ -12,7 +12,6 @@ "Unity.Mathematics.Extensions", "Unity.Networking.Transport", "Unity.Build", - "Unity.Jobs", "Unity.Logging" ], "includePlatforms": [], @@ -22,6 +21,32 @@ "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], - "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.logging", + "expression": "0.0", + "define": "USING_OBSOLETE_METHODS_VIA_INTERNALSVISIBLETO" + } + ], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Runtime/Variants/GhostTransformVariants.cs b/Runtime/Variants/GhostTransformVariants.cs index 3c1234c..40067c0 100644 --- a/Runtime/Variants/GhostTransformVariants.cs +++ b/Runtime/Variants/GhostTransformVariants.cs @@ -1,8 +1,139 @@ +using System.Collections.Generic; +using Unity.Entities; using Unity.Mathematics; +using Unity.Transforms; using UnityEngine.Scripting; namespace Unity.NetCode { +#if !ENABLE_TRANSFORM_V1 + /// + /// The default serialization strategy for the components provided by the NetCode package. + /// + [Preserve] + [GhostComponentVariation(typeof(Transforms.LocalTransform), "Transform - 3D")] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.AllClients)] + public struct TransformDefaultVariant + { + /// + /// The position value is replicated with a default quantization unit of 1000 (so roughly 1mm precision per component). + /// The replicated position value support both interpolation and extrapolation + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float3 Position; + + /// + /// The scale value is replicated with a default quantization unit of 1000. + /// The replicated scale value support both interpolation and extrapolation + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float Scale; + + /// + /// The rotation quaternion is replicated and the resulting floating point data use for replication the rotation is quantized with good precision (10 or more bits per component) + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public quaternion Rotation; + } + /// + /// A serialization strategy for that replicates only the entity + /// . + /// + [Preserve] + [GhostComponentVariation(typeof(Transforms.LocalTransform), "PositionOnly - 3D")] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.AllClients)] + public struct PositionOnlyVariant + { + /// + /// The position value is replicated with a default quantization unit of 1000 (so roughly 1mm precision per component). + /// The replicated position value support both interpolation and extrapolation + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float3 Position; + } + /// + /// A serialization strategy for that replicates only the entity + /// . + /// + [Preserve] + [GhostComponentVariation(typeof(Transforms.LocalTransform), "RotationOnly - 3D")] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.AllClients)] + public struct RotationOnlyVariant + { + /// + /// The rotation quaternion is replicated and the resulting floating point data use for replication the rotation is quantized with good precision (10 or more bits per component) + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public quaternion Rotation; + } + /// + /// A serialization strategy that replicates the entity and + /// properties. + /// + [Preserve] + [GhostComponentVariation(typeof(Transforms.LocalTransform), "PositionAndRotation - 3D")] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.AllClients)] + public struct PositionRotationVariant + { + /// + /// The position value is replicated with a default quantization unit of 1000 (so roughly 1mm precision per component). + /// The replicated position value support both interpolation and extrapolation + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float3 Position; + + /// + /// The position value is replicated with a default quantization unit of 100 (so roughly 1cm precision per component). + /// The replicated position value support both interpolation and extrapolation + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public quaternion Rotation; + } + /// + /// A serialization strategy that replicates the entity and + /// properties. + /// + [Preserve] + [GhostComponentVariation(typeof(Transforms.LocalTransform), "PositionScale - 3D")] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.AllClients)] + public struct PositionScaleVariant + { + /// + /// The position value is replicated with a default quantization unit of 1000 (so roughly 1mm precision per component). + /// The replicated position value support both interpolation and extrapolation + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float3 Position; + + /// + /// The scale value is replicated with a default quantization unit of 1000, and support both interpolation and exrapolation. + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float Scale; + } + /// + /// A serialization strategy that replicates the entity and + /// properties. + /// + [Preserve] + [GhostComponentVariation(typeof(Transforms.LocalTransform), "RotationScale - 3D")] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.AllClients)] + public struct RotationScaleVariant + { + /// + /// The position value is replicated with a default quantization unit of 1000 (so roughly 1mm precision per component). + /// The replicated position value support both interpolation and extrapolation + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public quaternion Rotation; + + /// + /// The scale value is replicated with a default quantization unit of 1000, and support both interpolation and exrapolation. + /// + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float Scale; + } +#else /// /// The default serialization strategy for the components provided by the NetCode package. /// @@ -15,7 +146,7 @@ public struct TranslationDefaultVariant /// The translation value is replicated with a default quantization unit of 100 (so roughly 1cm precision per component). /// The replicated translation value support both interpolation and extrapolation /// - [GhostField(Composite=true,Quantization=100, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] public float3 Value; + [GhostField(Composite=true,Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] public float3 Value; } /// @@ -31,4 +162,41 @@ public struct RotationDefaultVariant /// [GhostField(Composite=true,Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] public quaternion Value; } +#endif + + /// + /// System that optinally setup the Netcode default variants used for transform components in case a default is not already present. + /// The following variants are set by default by the package: + /// - if ENABLE_TRANSFORM_V1 is not set + /// - if ENABLE_TRANSFORM_V1 is set + /// - if ENABLE_TRANSFORM_V1 is set + /// + /// It will never override the default assignment for the transform components if they are already present in the + /// map. + /// Any system deriving from will take precendence, even if they are created + /// after this system. + /// + /// + [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation | + WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.BakingSystem)] + [CreateAfter(typeof(GhostComponentSerializerCollectionSystemGroup))] + [UpdateInGroup(typeof(DefaultVariantSystemGroup), OrderLast = true)] + public sealed partial class TransformDefaultVariantSystem : SystemBase + { + protected override void OnCreate() + { + var rules = World.GetExistingSystemManaged().DefaultVariantRules; +#if !ENABLE_TRANSFORM_V1 + rules.TrySetDefaultVariant(ComponentType.ReadWrite(), DefaultVariantSystemBase.Rule.OnlyParents(typeof(TransformDefaultVariant)), this); +#else + rules.TrySetDefaultVariant(ComponentType.ReadWrite(), DefaultVariantSystemBase.Rule.OnlyParents(typeof(TranslationDefaultVariant)), this); + rules.TrySetDefaultVariant(ComponentType.ReadWrite(), DefaultVariantSystemBase.Rule.OnlyParents(typeof(RotationDefaultVariant)), this); +#endif + Enabled = false; + } + + protected override void OnUpdate() + { + } + } } diff --git a/Tests/Editor/BootstrapTests.cs b/Tests/Editor/BootstrapTests.cs index 8f83f1f..d961559 100644 --- a/Tests/Editor/BootstrapTests.cs +++ b/Tests/Editor/BootstrapTests.cs @@ -40,13 +40,37 @@ protected override void OnUpdate() public enum SendForChildrenTestCase { - YesViaDefaultNameDictionary, + /// Creating a child overload via . + YesViaDefaultVariantMap, + /// Creating a child overload via . + NoViaDefaultVariantMap, + /// Using the to define an override on a child. YesViaInspectionComponentOverride, - NoViaDontSerializeVariantDefault, + /// Children default to . + Default, + + // TODO - Tests for ClientOnlyVariant. } public class BootstrapTests { + internal static bool IsExpectedToBeReplicated(SendForChildrenTestCase sendForChildrenTestCase, bool isRoot) + { + switch (sendForChildrenTestCase) + { + case SendForChildrenTestCase.YesViaDefaultVariantMap: + return true; + case SendForChildrenTestCase.NoViaDefaultVariantMap: + return false; + case SendForChildrenTestCase.YesViaInspectionComponentOverride: + return true; + case SendForChildrenTestCase.Default: + return isRoot; + default: + throw new ArgumentOutOfRangeException(nameof(sendForChildrenTestCase), sendForChildrenTestCase, nameof(IsExpectedToBeReplicated)); + } + } + [Test] public void BootstrapRespectsUpdateInWorld() { diff --git a/Tests/Editor/CommandBufferSerialization.cs b/Tests/Editor/CommandBufferSerialization.cs index c0d1f85..a8c6c61 100644 --- a/Tests/Editor/CommandBufferSerialization.cs +++ b/Tests/Editor/CommandBufferSerialization.cs @@ -45,9 +45,18 @@ protected override void OnCreate() } protected override void OnUpdate() { - var tick = GetSingleton().ServerTick; + var tick = SystemAPI.GetSingleton().ServerTick; Entities .WithAll() +#if !ENABLE_TRANSFORM_V1 + .ForEach((Entity entity, ref LocalTransform transform, in DynamicBuffer inputBuffer) => + { + if (!inputBuffer.GetDataAtTick(tick, out var input)) + return; + + transform.Position.y += 1.0f * input.Value; + }).Run(); +#else .ForEach((Entity entity, ref Translation translation, in DynamicBuffer inputBuffer) => { if (!inputBuffer.GetDataAtTick(tick, out var input)) @@ -55,6 +64,7 @@ protected override void OnUpdate() translation.Value.y += 1.0f * input.Value; }).Run(); +#endif } } @@ -71,14 +81,14 @@ protected override void OnCreate() } protected override void OnUpdate() { - var connection = GetSingletonEntity(); + var connection = SystemAPI.GetSingletonEntity(); var commandTarget = EntityManager.GetComponentData(connection); if (commandTarget.targetEntity == Entity.Null) return; var inputBuffer = EntityManager.GetBuffer(commandTarget.targetEntity); inputBuffer.AddCommandData(new TestInput { - Tick = GetSingleton().ServerTick, + Tick = SystemAPI.GetSingleton().ServerTick, Value = 1 }); } diff --git a/Tests/Editor/CommandDataTests.cs b/Tests/Editor/CommandDataTests.cs index 4178764..4f6c86c 100644 --- a/Tests/Editor/CommandDataTests.cs +++ b/Tests/Editor/CommandDataTests.cs @@ -33,7 +33,7 @@ protected override void OnCreate() } protected override void OnUpdate() { - var tick = GetSingleton().ServerTick; + var tick = SystemAPI.GetSingleton().ServerTick; Entities.ForEach((DynamicBuffer inputBuffer) => { inputBuffer.AddCommandData(new CommandDataTestsTickInput { @@ -55,7 +55,7 @@ protected override void OnCreate() } protected override void OnUpdate() { - var tick = GetSingleton().ServerTick; + var tick = SystemAPI.GetSingleton().ServerTick; Entities.ForEach((DynamicBuffer inputBuffer) => { inputBuffer.AddCommandData(new CommandDataTestsTickInput2 { diff --git a/Tests/Editor/DotsGlobalSettingsTests.cs b/Tests/Editor/DotsGlobalSettingsTests.cs index e43a43e..4b08471 100644 --- a/Tests/Editor/DotsGlobalSettingsTests.cs +++ b/Tests/Editor/DotsGlobalSettingsTests.cs @@ -1,6 +1,6 @@ -using System.IO; +using System.IO; using System.Linq; -using Authoring.Hybrid; +using Unity.NetCode.Hybrid; using UnityEditor; using NUnit.Framework; using Unity.Entities.Build; @@ -15,21 +15,6 @@ namespace Unity.Scenes.Editor.Tests { public class DotsGlobalSettingsTests : TestWithSceneAsset { - private bool m_PreviousBuiltInEnabledOption; - - [SetUp] - public void Setup() - { - m_PreviousBuiltInEnabledOption = LiveConversionSettings.IsBuiltinBuildsEnabled; - LiveConversionSettings.IsBuiltinBuildsEnabled = true; - } - - [TearDown] - public void Teardown() - { - LiveConversionSettings.IsBuiltinBuildsEnabled = m_PreviousBuiltInEnabledOption; - } - [Test] public void SuccessfulClientBuildTest() { @@ -60,7 +45,6 @@ public void SuccessfulClientBuildTest() buildOptions.locationPathName = uniqueTempPath + "/Test.app"; buildOptions.extraScriptingDefines = new string[] {"UNITY_CLIENT"}; - dotsSettings.SetPlayerType(DotsGlobalSettings.PlayerType.Client); ((ClientSettings) dotsSettings.ClientProvider).NetCodeClientTarget = NetCodeClientTarget.Client; var report = BuildPipeline.BuildPlayer(buildOptions); @@ -69,9 +53,7 @@ public void SuccessfulClientBuildTest() } finally { - dotsSettings.SetPlayerType(originalPlayerType); - ((Authoring.Hybrid.ClientSettings) dotsSettings.ClientProvider).NetCodeClientTarget = originalNetCodeClientTarget; - Teardown(); + ((Unity.NetCode.Hybrid.ClientSettings) dotsSettings.ClientProvider).NetCodeClientTarget = originalNetCodeClientTarget; } } @@ -103,7 +85,6 @@ public void SuccessfulClientAndServerBuildTest() if(isOSXEditor) buildOptions.locationPathName = uniqueTempPath + "/Test.app"; - dotsSettings.SetPlayerType(DotsGlobalSettings.PlayerType.Client); ((ClientSettings) dotsSettings.ClientProvider).NetCodeClientTarget = NetCodeClientTarget.ClientAndServer; var report = BuildPipeline.BuildPlayer(buildOptions); @@ -112,12 +93,11 @@ public void SuccessfulClientAndServerBuildTest() } finally { - dotsSettings.SetPlayerType(originalPlayerType); - ((Authoring.Hybrid.ClientSettings) dotsSettings.ClientProvider).NetCodeClientTarget = originalNetCodeClientTarget; - Teardown(); + ((Unity.NetCode.Hybrid.ClientSettings) dotsSettings.ClientProvider).NetCodeClientTarget = originalNetCodeClientTarget; } } + static void EnsureResourceCatalogHasBeenDeployed(string uniqueTempPath, bool isOSXEditor, BuildReport report) { var locationPath = Application.dataPath + "/../" + uniqueTempPath; @@ -126,7 +106,7 @@ static void EnsureResourceCatalogHasBeenDeployed(string uniqueTempPath, bool isO streamingAssetPath = locationPath + $"/Test.app/Contents/Resources/Data/StreamingAssets/"; // REDO: Just check the resource catalog has been deployed - var sceneInfoFileRelativePath = streamingAssetPath + EntityScenesPaths.k_SceneInfoFileName; + var sceneInfoFileRelativePath = EntityScenesPaths.FullPathForFile(streamingAssetPath, EntityScenesPaths.RelativePathForSceneInfoFile); var resourceCatalogFileExists = File.Exists(sceneInfoFileRelativePath); var reportMessages = string.Join('\n', report.steps.SelectMany(x => x.messages).Select(x => $"[{x.type}] {x.content}")); var stringReport = $"[{report.summary.result}, {report.summary.totalErrors} totalErrors, {report.summary.totalWarnings} totalWarnings, resourceCatalogFileExists: {resourceCatalogFileExists}]\nBuild logs ----------\n{reportMessages} ------ "; diff --git a/Tests/Editor/ExtrapolationTests.cs b/Tests/Editor/ExtrapolationTests.cs index 5a27945..c74d896 100644 --- a/Tests/Editor/ExtrapolationTests.cs +++ b/Tests/Editor/ExtrapolationTests.cs @@ -64,8 +64,8 @@ public partial class CheckExtrapolate : SystemBase { protected override void OnUpdate() { - var InterpolTick = GetSingleton().InterpolationTick; - var InterpolFraction = GetSingleton().InterpolationTickFraction; + var InterpolTick = SystemAPI.GetSingleton().InterpolationTick; + var InterpolFraction = SystemAPI.GetSingleton().InterpolationTickFraction; Entities.WithoutBurst().ForEach((ref TestExtrapolated val, ref ExtrapolateBackup bkup) => { if (bkup.Tick == InterpolTick && bkup.Fraction == InterpolFraction) return; diff --git a/Tests/Editor/GhostCollectionStreamingTests.cs b/Tests/Editor/GhostCollectionStreamingTests.cs index 21aa472..c9a9943 100644 --- a/Tests/Editor/GhostCollectionStreamingTests.cs +++ b/Tests/Editor/GhostCollectionStreamingTests.cs @@ -28,7 +28,7 @@ public partial class OnDemandLoadTestSystem : SystemBase public bool IsLoading = false; protected override void OnUpdate() { - var collectionEntity = GetSingletonEntity(); + var collectionEntity = SystemAPI.GetSingletonEntity(); var ghostCollection = EntityManager.GetBuffer(collectionEntity); // This must be done on the main thread for now diff --git a/Tests/Editor/GhostGenTestTypes.cs b/Tests/Editor/GhostGenTestTypes.cs index 86b15eb..80d74a8 100644 --- a/Tests/Editor/GhostGenTestTypes.cs +++ b/Tests/Editor/GhostGenTestTypes.cs @@ -59,6 +59,12 @@ public struct GhostGenTestTypeFlat : IComponentData [GhostField(Quantization=10)] public float FloatValue; [GhostField(Quantization=10, Smoothing=SmoothingAction.Interpolate)] public float Interpolated_FloatValue; + + [GhostField] public double Unquantized_DoubleValue; + [GhostField(Smoothing=SmoothingAction.Interpolate)] public double Unquantized_Interpolated_DoubleValue; + [GhostField(Quantization=10)] public float DoubleValue; + [GhostField(Quantization=10, Smoothing=SmoothingAction.Interpolate)] public float Interpolated_DoubleValue; + [GhostField(Quantization=10)] public float2 Float2Value; [GhostField(Quantization=10, Smoothing=SmoothingAction.Interpolate)] public float2 Interpolated_Float2Value; [GhostField] public float2 Unquantized_Float2Value; @@ -136,6 +142,11 @@ void VerifyGhostValues(NetCodeTestWorld testWorld) Assert.AreEqual(serverValues.Unquantized_FloatValue, clientValues.Unquantized_FloatValue); Assert.AreEqual(serverValues.Unquantized_Interpolated_FloatValue, clientValues.Unquantized_Interpolated_FloatValue); + Assert.AreEqual(serverValues.DoubleValue, clientValues.DoubleValue); + Assert.AreEqual(serverValues.Interpolated_DoubleValue, clientValues.Interpolated_DoubleValue); + Assert.AreEqual(serverValues.Unquantized_DoubleValue, clientValues.Unquantized_DoubleValue); + Assert.AreEqual(serverValues.Unquantized_Interpolated_DoubleValue, clientValues.Unquantized_Interpolated_DoubleValue); + Assert.AreEqual(serverValues.Float2Value, clientValues.Float2Value); Assert.AreEqual(serverValues.Interpolated_Float2Value, clientValues.Interpolated_Float2Value); Assert.AreEqual(serverValues.Unquantized_Float2Value, clientValues.Unquantized_Float2Value); @@ -232,6 +243,11 @@ void SetGhostValues(NetCodeTestWorld testWorld, int baseValue) Unquantized_FloatValue = baseValue + ++i, Unquantized_Interpolated_FloatValue = baseValue + ++i, + DoubleValue = baseValue + ++i, + Interpolated_DoubleValue = baseValue + ++i, + Unquantized_DoubleValue = baseValue + ++i, + Unquantized_Interpolated_DoubleValue = baseValue + ++i, + Float2Value = new float2(baseValue + ++i, baseValue + ++i), Interpolated_Float2Value = new float2(baseValue + ++i, baseValue + ++i), Unquantized_Float2Value = new float2(baseValue + ++i, baseValue + ++i), diff --git a/Tests/Editor/GhostSerializationDataForEnableableBits.cs b/Tests/Editor/GhostSerializationDataForEnableableBits.cs index 403350e..254fa67 100644 --- a/Tests/Editor/GhostSerializationDataForEnableableBits.cs +++ b/Tests/Editor/GhostSerializationDataForEnableableBits.cs @@ -1,6 +1,9 @@ +using System; using NUnit.Framework; using Unity.Entities; using UnityEngine; +// ReSharper disable InconsistentNaming +// ReSharper disable ParameterHidesMember namespace Unity.NetCode.Tests { @@ -14,10 +17,11 @@ public enum GhostTypes MultipleEnableableBuffer, ChildComponent, ChildBufferComponent, - GhostGroup + GhostGroup, + // TODO: Support GhostGroupBuffers! } - private GhostTypes type; + GhostTypes type; public GhostTypeConverter(GhostTypes ghostType) { type = ghostType; @@ -28,7 +32,7 @@ public void Bake(GameObject gameObject, IBaker baker) { case GhostTypes.EnableableComponent: baker.AddComponent(new GhostOwnerComponent()); - baker.AddComponent(new EnableableComponent{}); + AddTestEnableableComponents(baker); break; case GhostTypes.MultipleEnableableComponent: baker.AddComponent(new GhostOwnerComponent()); @@ -36,7 +40,8 @@ public void Bake(GameObject gameObject, IBaker baker) break; case GhostTypes.EnableableBuffer: baker.AddComponent(new GhostOwnerComponent()); - baker.AddBuffer(); + AddBufferWithLength(baker); + // TODO - Same tests for buffers. break; case GhostTypes.MultipleEnableableBuffer: baker.AddComponent(new GhostOwnerComponent()); @@ -44,7 +49,7 @@ public void Bake(GameObject gameObject, IBaker baker) break; case GhostTypes.ChildComponent: baker.AddComponent(new GhostOwnerComponent()); - baker.AddComponent(new EnableableComponent{}); + AddTestEnableableComponents(baker); var transform = baker.GetComponent(); baker.DependsOn(transform.parent); if (transform.parent == null) @@ -52,7 +57,7 @@ public void Bake(GameObject gameObject, IBaker baker) break; case GhostTypes.ChildBufferComponent: baker.AddComponent(new GhostOwnerComponent()); - baker.AddBuffer(); + AddBufferWithLength(baker); if (gameObject.transform.parent == null) baker.AddComponent(new TopLevelGhostEntity()); break; @@ -60,16 +65,16 @@ public void Bake(GameObject gameObject, IBaker baker) baker.AddComponent(new GhostOwnerComponent()); // Dependency on the name baker.DependsOn(gameObject); - if (gameObject.name == "ParentGhost") + if (gameObject.name.StartsWith("ParentGhost")) { baker.AddBuffer(); baker.AddComponent(default(GhostGroupRoot)); - baker.AddComponent(default(EnableableComponent)); + AddTestEnableableComponents(baker); } else { baker.AddComponent(default(GhostChildEntityComponent)); - baker.AddComponent(default(EnableableComponent)); + AddTestEnableableComponents(baker); } break; default: @@ -78,152 +83,180 @@ public void Bake(GameObject gameObject, IBaker baker) } } - static void SetupMultipleEnableableComponents(Entity entity, EntityManager dstManager) - { - dstManager.AddComponentData(entity, new EnableableComponent_0()); - dstManager.AddComponentData(entity, new EnableableComponent_1()); - dstManager.AddComponentData(entity, new EnableableComponent_2()); - dstManager.AddComponentData(entity, new EnableableComponent_3()); - dstManager.AddComponentData(entity, new EnableableComponent_4()); - dstManager.AddComponentData(entity, new EnableableComponent_5()); - dstManager.AddComponentData(entity, new EnableableComponent_6()); - dstManager.AddComponentData(entity, new EnableableComponent_7()); - dstManager.AddComponentData(entity, new EnableableComponent_8()); - dstManager.AddComponentData(entity, new EnableableComponent_9()); - dstManager.AddComponentData(entity, new EnableableComponent_10()); - dstManager.AddComponentData(entity, new EnableableComponent_11()); - dstManager.AddComponentData(entity, new EnableableComponent_12()); - dstManager.AddComponentData(entity, new EnableableComponent_13()); - dstManager.AddComponentData(entity, new EnableableComponent_14()); - dstManager.AddComponentData(entity, new EnableableComponent_15()); - dstManager.AddComponentData(entity, new EnableableComponent_16()); - dstManager.AddComponentData(entity, new EnableableComponent_17()); - dstManager.AddComponentData(entity, new EnableableComponent_18()); - dstManager.AddComponentData(entity, new EnableableComponent_19()); - dstManager.AddComponentData(entity, new EnableableComponent_20()); - dstManager.AddComponentData(entity, new EnableableComponent_21()); - dstManager.AddComponentData(entity, new EnableableComponent_22()); - dstManager.AddComponentData(entity, new EnableableComponent_23()); - dstManager.AddComponentData(entity, new EnableableComponent_24()); - dstManager.AddComponentData(entity, new EnableableComponent_25()); - dstManager.AddComponentData(entity, new EnableableComponent_26()); - dstManager.AddComponentData(entity, new EnableableComponent_27()); - dstManager.AddComponentData(entity, new EnableableComponent_28()); - dstManager.AddComponentData(entity, new EnableableComponent_29()); - dstManager.AddComponentData(entity, new EnableableComponent_30()); - dstManager.AddComponentData(entity, new EnableableComponent_31()); - dstManager.AddComponentData(entity, new EnableableComponent_32()); + /// Item1 is the ComponentType. Item2 is the VariantType (or null if same as ComponentType). + internal static ValueTuple[] FetchAllTestComponentTypesRequiringSendRuleOverride() + { + return new[] + { + (typeof(EnableableComponent), null), + (typeof(EnableableFlagComponent), null), + (typeof(ReplicatedFieldWithNonReplicatedEnableableComponent), null), + (typeof(ReplicatedEnableableComponentWithNonReplicatedField), null), + (typeof(ComponentWithVariant), typeof(ComponentWithVariantVariation)), + (typeof(ComponentWithNonReplicatedVariant), typeof(ComponentWithNonReplicatedVariantVariation)), + // Skipped as never replicated. (typeof(NeverReplicatedEnableableFlagComponent), null), + + (typeof(EnableableComponent_0), null), + (typeof(EnableableComponent_1), null), + (typeof(EnableableComponent_2), null), + (typeof(EnableableComponent_3), null), + (typeof(EnableableComponent_4), null), + (typeof(EnableableComponent_5), null), + (typeof(EnableableComponent_6), null), + (typeof(EnableableComponent_7), null), + (typeof(EnableableComponent_8), null), + (typeof(EnableableComponent_9), null), + (typeof(EnableableComponent_10), null), + (typeof(EnableableComponent_11), null), + (typeof(EnableableComponent_12), null), + (typeof(EnableableComponent_13), null), + (typeof(EnableableComponent_14), null), + (typeof(EnableableComponent_15), null), + (typeof(EnableableComponent_16), null), + (typeof(EnableableComponent_17), null), + (typeof(EnableableComponent_18), null), + (typeof(EnableableComponent_19), null), + (typeof(EnableableComponent_20), null), + (typeof(EnableableComponent_21), null), + (typeof(EnableableComponent_22), null), + (typeof(EnableableComponent_23), null), + (typeof(EnableableComponent_24), null), + (typeof(EnableableComponent_25), null), + (typeof(EnableableComponent_26), null), + (typeof(EnableableComponent_27), null), + (typeof(EnableableComponent_28), null), + (typeof(EnableableComponent_29), null), + (typeof(EnableableComponent_30), null), + (typeof(EnableableComponent_31), null), + (typeof(EnableableComponent_32), null), + + (typeof(EnableableBuffer), null), + (typeof(EnableableBuffer_0), null), + (typeof(EnableableBuffer_1), null), + (typeof(EnableableBuffer_2), null), + (typeof(EnableableBuffer_3), null), + (typeof(EnableableBuffer_4), null), + (typeof(EnableableBuffer_5), null), + (typeof(EnableableBuffer_6), null), + (typeof(EnableableBuffer_7), null), + (typeof(EnableableBuffer_8), null), + (typeof(EnableableBuffer_9), null), + (typeof(EnableableBuffer_10), null), + (typeof(EnableableBuffer_11), null), + (typeof(EnableableBuffer_12), null), + (typeof(EnableableBuffer_13), null), + (typeof(EnableableBuffer_14), null), + (typeof(EnableableBuffer_15), null), + (typeof(EnableableBuffer_16), null), + (typeof(EnableableBuffer_17), null), + (typeof(EnableableBuffer_18), null), + (typeof(EnableableBuffer_19), null), + (typeof(EnableableBuffer_20), null), + (typeof(EnableableBuffer_21), null), + (typeof(EnableableBuffer_22), null), + (typeof(EnableableBuffer_23), null), + (typeof(EnableableBuffer_24), null), + (typeof(EnableableBuffer_25), null), + (typeof(EnableableBuffer_26), null), + (typeof(EnableableBuffer_27), null), + (typeof(EnableableBuffer_28), null), + (typeof(EnableableBuffer_29), null), + (typeof(EnableableBuffer_30), null), + (typeof(EnableableBuffer_31), null), + (typeof(EnableableBuffer_32), null), + }; + } + + static void AddTestEnableableComponents(IBaker baker) + { + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); } static void SetupMultipleEnableableComponents(IBaker baker) { - baker.AddComponent(new EnableableComponent_0()); - baker.AddComponent(new EnableableComponent_1()); - baker.AddComponent(new EnableableComponent_2()); - baker.AddComponent(new EnableableComponent_3()); - baker.AddComponent(new EnableableComponent_4()); - baker.AddComponent(new EnableableComponent_5()); - baker.AddComponent(new EnableableComponent_6()); - baker.AddComponent(new EnableableComponent_7()); - baker.AddComponent(new EnableableComponent_8()); - baker.AddComponent(new EnableableComponent_9()); - baker.AddComponent(new EnableableComponent_10()); - baker.AddComponent(new EnableableComponent_11()); - baker.AddComponent(new EnableableComponent_12()); - baker.AddComponent(new EnableableComponent_13()); - baker.AddComponent(new EnableableComponent_14()); - baker.AddComponent(new EnableableComponent_15()); - baker.AddComponent(new EnableableComponent_16()); - baker.AddComponent(new EnableableComponent_17()); - baker.AddComponent(new EnableableComponent_18()); - baker.AddComponent(new EnableableComponent_19()); - baker.AddComponent(new EnableableComponent_20()); - baker.AddComponent(new EnableableComponent_21()); - baker.AddComponent(new EnableableComponent_22()); - baker.AddComponent(new EnableableComponent_23()); - baker.AddComponent(new EnableableComponent_24()); - baker.AddComponent(new EnableableComponent_25()); - baker.AddComponent(new EnableableComponent_26()); - baker.AddComponent(new EnableableComponent_27()); - baker.AddComponent(new EnableableComponent_28()); - baker.AddComponent(new EnableableComponent_29()); - baker.AddComponent(new EnableableComponent_30()); - baker.AddComponent(new EnableableComponent_31()); - baker.AddComponent(new EnableableComponent_32()); - } - - static void SetupMultipleEnableableBuffer(Entity entity, EntityManager dstManager) - { - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); - dstManager.AddBuffer(entity); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + baker.AddComponent(); + } + + static void AddBufferWithLength(IBaker baker) + where T : unmanaged, IBufferElementData + { + var enableableBuffers = baker.AddBuffer(); + enableableBuffers.Length = GhostSerializationTestsForEnableableBits.kClientBufferSize; } static void SetupMultipleEnableableBuffer(IBaker baker) { - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); - baker.AddBuffer(); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); + AddBufferWithLength(baker); } } @@ -234,6 +267,7 @@ public interface IComponentValue } [GhostComponent(SendDataForChildEntity = false)] + [GhostEnabledBit] public struct EnableableBuffer : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -248,6 +282,7 @@ public int GetValue() } } + [GhostEnabledBit] public struct EnableableComponent: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -263,8 +298,78 @@ public int GetValue() } } + /// Enable flag SHOULD BE replicated. + [GhostEnabledBit] + public struct EnableableFlagComponent : IComponentData, IEnableableComponent + { + } + + /// Enable flag should NOT BE replicated. + public struct NeverReplicatedEnableableFlagComponent : IComponentData, IEnableableComponent + { + } + + /// Enable flag should NOT BE replicated, but the field A SHOULD BE. + public struct ReplicatedFieldWithNonReplicatedEnableableComponent : IComponentData, IEnableableComponent, IComponentValue + { + [GhostField] + public int value; + + public void SetValue(int value) => this.value = value; + + public int GetValue() => value; + } + + /// Enable flag SHOULD BE replicated, but the field B should NOT BE. + [GhostEnabledBit] + public struct ReplicatedEnableableComponentWithNonReplicatedField : IComponentData, IEnableableComponent, IComponentValue + { + public int value; + + public void SetValue(int value) => this.value = value; + + public int GetValue() => value; + } + + public struct ComponentWithVariant : IComponentData, IEnableableComponent, IComponentValue + { + public int value; + + public void SetValue(int value) => this.value = value; + + public int GetValue() => value; + } + + // As this is the only variant, it becomes the default variant. + [GhostComponentVariation(typeof(ComponentWithVariant), "ReplicatedVariation")] + [GhostEnabledBit] + public struct ComponentWithVariantVariation + { + [GhostField] + public int value; + } + + [GhostEnabledBit] + public struct ComponentWithNonReplicatedVariant : IComponentData, IEnableableComponent, IComponentValue + { + [GhostField] + public int value; + + public void SetValue(int value) => this.value = value; + + public int GetValue() => value; + } + + // As this is the only variant, it becomes the default variant. + [GhostComponentVariation(typeof(ComponentWithNonReplicatedVariant), "NonReplicatedVariation")] + public struct ComponentWithNonReplicatedVariantVariation + { + public int value; + } + //////////////////////////////////////////////////////////////////////////// + [GhostEnabledBit] public struct EnableableComponent_0: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -279,6 +384,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_1: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -293,7 +399,7 @@ public int GetValue() return value; } } - + [GhostEnabledBit] public struct EnableableComponent_2: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -308,6 +414,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_3: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -322,6 +429,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_4: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -336,6 +444,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_5: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -351,6 +460,7 @@ public int GetValue() } } + [GhostEnabledBit] public struct EnableableComponent_6: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -365,6 +475,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_7: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -379,6 +490,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_8: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -393,6 +505,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_9: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -407,6 +520,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_10: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -421,6 +535,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_11: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -436,6 +551,7 @@ public int GetValue() } } + [GhostEnabledBit] public struct EnableableComponent_12: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -450,6 +566,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_13: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -464,6 +581,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_14: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -478,6 +596,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_15: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -493,6 +612,7 @@ public int GetValue() } } + [GhostEnabledBit] public struct EnableableComponent_16: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -507,6 +627,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_17: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -521,6 +642,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_18: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -535,6 +657,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_19: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -549,6 +672,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_20: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -563,6 +687,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_21: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -578,6 +703,7 @@ public int GetValue() } } + [GhostEnabledBit] public struct EnableableComponent_22: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -592,6 +718,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_23: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -606,6 +733,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_24: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -620,6 +748,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_25: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -635,6 +764,7 @@ public int GetValue() } } + [GhostEnabledBit] public struct EnableableComponent_26: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -649,6 +779,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_27: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -663,6 +794,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_28: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -677,6 +809,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_29: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -691,6 +824,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_30: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -705,6 +839,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_31: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -719,6 +854,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableComponent_32: IComponentData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -733,7 +869,7 @@ public int GetValue() return value; } } - + [GhostEnabledBit] public struct EnableableBuffer_0 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -747,6 +883,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_1 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -760,6 +897,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_2 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -773,6 +911,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_3 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -786,6 +925,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_4 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -799,6 +939,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_5 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -812,6 +953,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_6 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -825,6 +967,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_7 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -838,6 +981,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_8 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -851,6 +995,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_9 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -864,6 +1009,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_10 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -877,6 +1023,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_11 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -890,6 +1037,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_12 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -903,6 +1051,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_13 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -916,6 +1065,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_14 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -929,6 +1079,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_15 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -942,6 +1093,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_16 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -955,6 +1107,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_17 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -968,6 +1121,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_18 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -981,6 +1135,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_19 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -994,6 +1149,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_20 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1007,6 +1163,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_21 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1020,6 +1177,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_22 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1033,6 +1191,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_23 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1046,6 +1205,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_24 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1059,6 +1219,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_25 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1072,6 +1233,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_26 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1085,6 +1247,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_27 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1098,6 +1261,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_28 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1111,6 +1275,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_29 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1124,6 +1289,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_30 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1137,6 +1303,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_31 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; @@ -1150,6 +1317,7 @@ public int GetValue() return value; } } + [GhostEnabledBit] public struct EnableableBuffer_32 : IBufferElementData, IEnableableComponent, IComponentValue { [GhostField] public int value; diff --git a/Tests/Editor/GhostSerializationTests.cs b/Tests/Editor/GhostSerializationTests.cs index 8b649b6..99d19e8 100644 --- a/Tests/Editor/GhostSerializationTests.cs +++ b/Tests/Editor/GhostSerializationTests.cs @@ -76,6 +76,8 @@ public struct GhostValueSerializer : IComponentData [GhostField(Quantization=10)] public float FloatValue; [GhostField(Quantization=0)] public float UnquantizedFloatValue; + [GhostField(Quantization=1000)] public double DoubleValue; + [GhostField(Quantization=0)] public double UnquantizedDoubleValue; [GhostField(Quantization=10)] public float2 Float2Value; [GhostField(Quantization=0)] public float2 UnquantizedFloat2Value; [GhostField(Quantization=10)] public float3 Float3Value; @@ -110,6 +112,8 @@ void VerifyGhostValues(NetCodeTestWorld testWorld) Assert.AreEqual(serverValues.ULongValue, clientValues.ULongValue); Assert.AreEqual(serverValues.FloatValue, clientValues.FloatValue); Assert.AreEqual(serverValues.UnquantizedFloatValue, clientValues.UnquantizedFloatValue); + Assert.AreEqual(serverValues.UnquantizedDoubleValue, clientValues.UnquantizedDoubleValue); + Assert.LessOrEqual(math.distance(serverValues.DoubleValue, clientValues.DoubleValue), 1e-3); Assert.AreEqual(serverValues.EnumUntyped,clientValues.EnumUntyped); Assert.AreEqual(serverValues.EnumS08,clientValues.EnumS08); @@ -152,6 +156,8 @@ void SetGhostValues(NetCodeTestWorld testWorld, int baseValue) ULongValue = ((ulong)baseValue) + 0x8234567898763210UL, FloatValue = baseValue + 2, UnquantizedFloatValue = baseValue + 3, + DoubleValue = 1234.456 + baseValue, + UnquantizedDoubleValue = 123456789.123456789 + baseValue, EnumUntyped = EnumUntyped.Value0, EnumS08 = EnumS8.Value0, diff --git a/Tests/Editor/GhostSerializationTestsForEnableableBits.cs b/Tests/Editor/GhostSerializationTestsForEnableableBits.cs index 2363720..0ee1bcf 100644 --- a/Tests/Editor/GhostSerializationTestsForEnableableBits.cs +++ b/Tests/Editor/GhostSerializationTestsForEnableableBits.cs @@ -14,125 +14,9 @@ public class GhostSerializationTestsForEnableableBits { float frameTime = 1.0f / 60.0f; - int GetClientEntityCount() + void TickMultipleFrames() { - var type = ComponentType.ReadOnly(); - var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(type); - return query.CalculateEntityCountWithoutFiltering(); - } - - int TickUntilReplicationIsDone(bool enabledBit) - { - int framesElapsed = 0; - - var clientCount = 0; - do - { - testWorld.Tick(frameTime); - framesElapsed++; - - switch (type) - { - case GhostTypeConverter.GhostTypes.EnableableComponent: - clientCount = GetClientEntityCount(); - break; - case GhostTypeConverter.GhostTypes.MultipleEnableableComponent: - clientCount = GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - break; - case GhostTypeConverter.GhostTypes.EnableableBuffer: - clientCount = GetClientEntityCount(); - break; - case GhostTypeConverter.GhostTypes.MultipleEnableableBuffer: - clientCount = GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - clientCount &= GetClientEntityCount(); - break; - case GhostTypeConverter.GhostTypes.ChildComponent: - clientCount = GetClientEntityCount(); - break; - case GhostTypeConverter.GhostTypes.ChildBufferComponent: - clientCount = GetClientEntityCount(); - break; - case GhostTypeConverter.GhostTypes.GhostGroup: - clientCount = GetClientEntityCount(); - clientCount += GetClientEntityCount(); - break; - default: - Assert.True(true); - break; - } - Assert.AreNotEqual(framesElapsed, 256, "Took way to long"); - } while (clientCount != serverEntities.Length); - - return framesElapsed * 2; - } - - void TickMultipleFrames(int count) - { - Assert.True(count < 256); - for (int i = 0; i < count; ++i) + for (int i = 0; i < 20; ++i) { testWorld.Tick(frameTime); } @@ -141,57 +25,83 @@ void TickMultipleFrames(int count) void SetLinkedBufferValues(int value, bool enabled) where T : unmanaged, IBufferElementData, IEnableableComponent, IComponentValue { - foreach (var entity in serverEntities) + foreach (var serverEntity in serverEntities) { - var serverEntityGroup = testWorld.ServerWorld.EntityManager.GetBuffer(entity); + var serverEntityGroup = testWorld.ServerWorld.EntityManager.GetBuffer(serverEntity); Assert.AreEqual(2, serverEntityGroup.Length); testWorld.ServerWorld.EntityManager.SetComponentEnabled(serverEntityGroup[0].Value, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[0].Value) == enabled); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[0].Value), $"{typeof(T)} is set correctly on server, linked[0]"); testWorld.ServerWorld.EntityManager.SetComponentEnabled(serverEntityGroup[1].Value, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[1].Value) == enabled); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[1].Value), $"{typeof(T)} is set correctly on server, linked[1]"); - var serverBuffer = testWorld.ServerWorld.EntityManager.GetBuffer(entity); + SetupBuffer(testWorld.ServerWorld.EntityManager.GetBuffer(serverEntityGroup[0].Value)); + SetupBuffer(testWorld.ServerWorld.EntityManager.GetBuffer(serverEntityGroup[1].Value)); - serverBuffer.ResizeUninitialized(kBufferSize); - for (int i = 0; i < kBufferSize; ++i) + void SetupBuffer(DynamicBuffer buffer) { - var newValue = new T(); - newValue.SetValue((i + 1) * 1000 + value); - - serverBuffer[i] = newValue; + buffer.ResizeUninitialized(kServerBufferSize); + for (int i = 0; i < kServerBufferSize; ++i) + { + var newValue = new T(); + newValue.SetValue((i + 1) * 1000 + value); + buffer[i] = newValue; + } } } } void SetGhostGroupValues(int value, bool enabled) where T : unmanaged, IComponentData, IEnableableComponent, IComponentValue + { + SetGhostGroupEnabled(enabled); + for (int i = 0; i < serverEntities.Length; i += 2) + { + var rootEntity = serverEntities[i]; + var childEntity = serverEntities[i + 1]; + T newValue = default; + newValue.SetValue(value); + testWorld.ServerWorld.EntityManager.SetComponentData(rootEntity, newValue); + testWorld.ServerWorld.EntityManager.SetComponentData(childEntity, newValue); + } + } + + void SetGhostGroupEnabled(bool enabled) + where T : unmanaged, IComponentData, IEnableableComponent { for (int i = 0; i < serverEntities.Length; i += 2) { var rootEntity = serverEntities[i]; - var childEntity = serverEntities[i+1]; + var childEntity = serverEntities[i + 1]; Assert.True(testWorld.ServerWorld.EntityManager.HasComponent(rootEntity)); Assert.True(testWorld.ServerWorld.EntityManager.HasComponent(childEntity)); testWorld.ServerWorld.EntityManager.SetComponentEnabled(rootEntity, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(rootEntity) == enabled); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(rootEntity), $"{typeof(T)} is set correctly on server, root entity"); testWorld.ServerWorld.EntityManager.SetComponentEnabled(childEntity, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(childEntity) == enabled); - - var newValue = new T(); - newValue.SetValue(value); - - testWorld.ServerWorld.EntityManager.SetComponentData(rootEntity, newValue); - testWorld.ServerWorld.EntityManager.SetComponentData(childEntity, newValue); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(childEntity), $"{typeof(T)} is set correctly on server, child entity"); } } void SetLinkedComponentValues(int value, bool enabled) where T : unmanaged, IComponentData, IEnableableComponent, IComponentValue + { + SetLinkedComponentEnabled(enabled); + foreach (var entity in serverEntities) + { + var serverEntityGroup = testWorld.ServerWorld.EntityManager.GetBuffer(entity); + T newValue = default; + newValue.SetValue(value); + testWorld.ServerWorld.EntityManager.SetComponentData(serverEntityGroup[0].Value, newValue); + testWorld.ServerWorld.EntityManager.SetComponentData(serverEntityGroup[1].Value, newValue); + } + } + + void SetLinkedComponentEnabled(bool enabled) + where T : unmanaged, IComponentData, IEnableableComponent { foreach (var entity in serverEntities) { @@ -199,45 +109,46 @@ void SetLinkedComponentValues(int value, bool enabled) Assert.AreEqual(2, serverEntityGroup.Length); testWorld.ServerWorld.EntityManager.SetComponentEnabled(serverEntityGroup[0].Value, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[0].Value) == enabled); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[0].Value), $"{typeof(T)} is set correctly on server, linked[0]"); testWorld.ServerWorld.EntityManager.SetComponentEnabled(serverEntityGroup[1].Value, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[1].Value) == enabled); - - var newValue = new T(); - newValue.SetValue(value); - - testWorld.ServerWorld.EntityManager.SetComponentData(serverEntityGroup[0].Value, newValue); - testWorld.ServerWorld.EntityManager.SetComponentData(serverEntityGroup[1].Value, newValue); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntityGroup[1].Value), $"{typeof(T)} is set correctly on server, linked[1]"); } } void SetComponentValues(int value, bool enabled) where T : unmanaged, IComponentData, IEnableableComponent, IComponentValue { + SetComponentEnabled(enabled); foreach (var entity in serverEntities) { - testWorld.ServerWorld.EntityManager.SetComponentEnabled(entity, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(entity) == enabled); - - var newValue = new T(); + T newValue = default; newValue.SetValue(value); - testWorld.ServerWorld.EntityManager.SetComponentData(entity, newValue); } } - private void SetBufferValues(int value, bool enabled) where T : unmanaged, IBufferElementData, IEnableableComponent, IComponentValue + void SetComponentEnabled(bool enabled) + where T : unmanaged, IComponentData, IEnableableComponent { foreach (var entity in serverEntities) { testWorld.ServerWorld.EntityManager.SetComponentEnabled(entity, enabled); - Assert.True(testWorld.ServerWorld.EntityManager.IsComponentEnabled(entity) == enabled); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(entity), $"{typeof(T)} is set correctly on server"); + } + } + + void SetBufferValues(int value, bool enabled) where T : unmanaged, IBufferElementData, IEnableableComponent, IComponentValue + { + foreach (var entity in serverEntities) + { + testWorld.ServerWorld.EntityManager.SetComponentEnabled(entity, enabled); + Assert.AreEqual(enabled, testWorld.ServerWorld.EntityManager.IsComponentEnabled(entity), $"{typeof(T)} buffer is set correctly on server"); var serverBuffer = testWorld.ServerWorld.EntityManager.GetBuffer(entity); - serverBuffer.ResizeUninitialized(kBufferSize); - for (int i = 0; i < kBufferSize; ++i) + serverBuffer.ResizeUninitialized(kServerBufferSize); + for (int i = 0; i < kServerBufferSize; ++i) { var newValue = new T(); newValue.SetValue((i + 1) * 1000 + value); @@ -248,8 +159,40 @@ void SetComponentValues(int value, bool enabled) } } - void VerifyGhostGroupValues(int value, bool enabled) + void VerifyGhostGroupValues(int expectedValue, bool expectedEnabled, SendForChildrenTestCase sendForChildrenTestCase) where T : unmanaged, IComponentData, IEnableableComponent, IComponentValue + { + VerifyGhostGroupEnabledBits(expectedEnabled, sendForChildrenTestCase); + + var rootType = ComponentType.ReadOnly(); + var childType = ComponentType.ReadOnly(); + + var rootQuery = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(rootType); + var childQuery = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(childType); + + using (var clientRootEntities = rootQuery.ToEntityArray(Allocator.TempJob)) + using (var clientChildEntities = childQuery.ToEntityArray(Allocator.TempJob)) + { + for (int i = 0; i < clientRootEntities.Length; i++) + { + var clientGroupRootEntity = clientRootEntities[i]; + var clientMemberEntity = clientChildEntities[i]; + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) // Ghost groups are root entities, by definition. + { + Assert.AreEqual(expectedValue, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientGroupRootEntity).GetValue(), $"[{typeof(T)}] ghost \"group root\" entity value IS replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(expectedValue, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientMemberEntity).GetValue(), $"[{typeof(T)}] ghost \"group member\" entity value when {sendForChildrenTestCase}"); + } + else + { + Assert.AreEqual(kDefaultValueIfNotReplicated, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientGroupRootEntity).GetValue(), $"[{typeof(T)}] ghost \"group root\" entity value is NOT replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(kDefaultValueIfNotReplicated, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientMemberEntity).GetValue(), $"[{typeof(T)}] ghost \"group member\" entity value is NOT replicated when {sendForChildrenTestCase}"); + } + } + } + } + + void VerifyGhostGroupEnabledBits(bool expectedEnabled, SendForChildrenTestCase sendForChildrenTestCase) + where T : unmanaged, IComponentData, IEnableableComponent { var rootType = ComponentType.ReadOnly(); var childType = ComponentType.ReadOnly(); @@ -265,20 +208,21 @@ void VerifyGhostGroupValues(int value, bool enabled) for (int i = 0; i < clientRootEntities.Length; i++) { - var clientRootEntity = clientRootEntities[i]; - var clientChildEntity = clientChildEntities[i]; + var clientGroupRootEntity = clientRootEntities[i]; + var clientGroupMemberEntity = clientChildEntities[i]; - var ent0 = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientRootEntity); - var ent1 = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientChildEntity); - - Assert.True(enabled == ent0); - Assert.True(enabled == ent1); - - Assert.AreEqual(value, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientRootEntity).GetValue()); - Assert.AreEqual(value, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientChildEntity).GetValue()); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) // Ghost groups are root entities, by definition. + { + Assert.AreEqual(expectedEnabled, testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientGroupRootEntity), $"[{typeof(T)}] ghost \"group root\" entity enabled IS replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(expectedEnabled, testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientGroupMemberEntity), $"[{typeof(T)}] ghost \"group member\" entity enabled IS replicated when {sendForChildrenTestCase}"); + } + else + { + Assert.AreEqual(kDefaultIfNotReplicated, testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientGroupRootEntity), $"[{typeof(T)}] ghost \"group root\" entity enabled NOT replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(kDefaultIfNotReplicated, testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientGroupMemberEntity), $"[{typeof(T)}] ghost \"group member\" entity enabled NOT replicated when {sendForChildrenTestCase}"); + } } } - } void VerifyLinkedBufferValues(int value, bool enabled, SendForChildrenTestCase sendForChildrenTestCase) @@ -296,181 +240,258 @@ void VerifyLinkedBufferValues(int value, bool enabled, SendForChildrenTestCas var serverEntity = serverEntities[i]; var clientEntity = clientEntities[i]; - Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(clientEntity)); + Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(clientEntity), "client linked group"); + + var serverEntityGroup = testWorld.ServerWorld.EntityManager.GetBuffer(serverEntity); var clientEntityGroup = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntity); - Assert.AreEqual(2, clientEntityGroup.Length); + Assert.AreEqual(2, clientEntityGroup.Length, "client linked group, expecting parent + child"); + + var clientParentEntityComponentEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntityGroup[0].Value); + var clientChildEntityComponentEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntityGroup[1].Value); + + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + Assert.AreEqual(enabled, clientParentEntityComponentEnabled, $"[{typeof(T)}] client parent entity component enabled bit IS replicated when {sendForChildrenTestCase}"); + } + else + { + Assert.AreEqual(kDefaultIfNotReplicated, clientParentEntityComponentEnabled, $"[{typeof(T)}] client parent entity component enabled bit NOT replicated when {sendForChildrenTestCase}"); + } - var ent0 = testWorld.ClientWorlds[0].EntityManager - .IsComponentEnabled(clientEntityGroup[0].Value); - var ent1 = testWorld.ClientWorlds[0].EntityManager - .IsComponentEnabled(clientEntityGroup[1].Value); + var serverParentBuffer = testWorld.ServerWorld.EntityManager.GetBuffer(serverEntityGroup[0].Value); + var serverChildBuffer = testWorld.ServerWorld.EntityManager.GetBuffer(serverEntityGroup[1].Value); + var clientParentBuffer = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntityGroup[0].Value); + var clientChildBuffer = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntityGroup[1].Value); - Assert.True(enabled == ent0); + // TODO: Make parent and child sizes different as further validation! + Assert.AreEqual(kServerBufferSize, serverParentBuffer.Length, $"[{typeof(T)}] server parent buffer length"); + Assert.AreEqual(kServerBufferSize, serverChildBuffer.Length, $"[{typeof(T)}] server child buffer length"); - var serverBuffer = testWorld.ServerWorld.EntityManager.GetBuffer(serverEntity); - var clientBuffer = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntity); + // Root: - Assert.AreEqual(kBufferSize, serverBuffer.Length); - Assert.IsTrue(clientBuffer.Length == serverBuffer.Length); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + Assert.AreEqual(kServerBufferSize, clientParentBuffer.Length, $"[{typeof(T)}] client parent buffer length IS replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(enabled, clientParentEntityComponentEnabled, $"[{typeof(T)}] client parent buffer enable bit IS replicated when {sendForChildrenTestCase}"); + } + else + { + Assert.AreEqual(kClientBufferSize, clientParentBuffer.Length, $"[{typeof(T)}] client parent buffer length NOT replicated when {sendForChildrenTestCase}, so using default client buffer length"); + Assert.AreEqual(kDefaultIfNotReplicated, clientParentEntityComponentEnabled, $"[{typeof(T)}] client parent buffer enable bit NOT replicated when {sendForChildrenTestCase}"); + } - if (sendForChildrenTestCase != SendForChildrenTestCase.NoViaDontSerializeVariantDefault) + for (int j = 0; j < serverParentBuffer.Length; ++j) { - Assert.True(enabled == ent1); - for (int j = 0; j < serverBuffer.Length; ++j) - { - var serverValue = serverBuffer[j]; - var clientValue = clientBuffer[j]; + var serverValue = serverParentBuffer[j]; + var clientValue = clientParentBuffer[j]; - var bufferValue = ((j + 1) * 1000 + value); - Assert.AreEqual(bufferValue, serverValue.GetValue()); - Assert.AreEqual(bufferValue, clientValue.GetValue()); + var bufferValue = ((j + 1) * 1000 + value); + Assert.AreEqual(bufferValue, serverValue.GetValue(), $"[{typeof(T)}] server parent value is written [{i}]"); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + Assert.AreEqual(bufferValue, clientValue.GetValue(), $"[{typeof(T)}] client parent value [{i}] expecting IS replicated when {sendForChildrenTestCase}"); + } + else + { + Assert.AreEqual(kDefaultValueIfNotReplicated, clientValue.GetValue(), $"[{typeof(T)}] client parent value [{i}] expecting NOT replicated when {sendForChildrenTestCase}"); } } + + // Children: + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, false)) + { + Assert.AreEqual(kServerBufferSize, clientChildBuffer.Length, $"[{typeof(T)}] client child buffer length IS replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(enabled, clientChildEntityComponentEnabled, $"[{typeof(T)}] client child buffer enable bit IS replicated when {sendForChildrenTestCase}"); + } else { - // TODO: Determine if enable-bits should be serialized when using `DontSerializeAttribute`. Sometimes they are? - //Assert.IsFalse(enabled == ent1); - for (int j = 0; j < clientBuffer.Length; ++j) - { - var serverValue = serverBuffer[j]; - var clientValue = clientBuffer[j]; + Assert.AreEqual(kClientBufferSize, clientChildBuffer.Length, $"[{typeof(T)}] client child buffer length NOT replicated when {sendForChildrenTestCase}, so will use the default client buffer length"); + Assert.AreEqual(kDefaultIfNotReplicated, clientChildEntityComponentEnabled, $"[{typeof(T)}] client child buffer enable bit NOT replicated when {sendForChildrenTestCase}"); + } + for (int j = 0; j < serverChildBuffer.Length; ++j) + { + var serverValue = serverChildBuffer[j]; + var clientValue = clientChildBuffer[j]; + + var bufferValue = ((j + 1) * 1000 + value); + Assert.AreEqual(bufferValue, serverValue.GetValue(), $"[{typeof(T)}] client child value is written [{i}]"); - var bufferValue = ((j + 1) * 1000 + value); - Assert.AreEqual(bufferValue, serverValue.GetValue()); - Assert.AreEqual(bufferValue, clientValue.GetValue()); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, false)) + { + Assert.AreEqual(bufferValue, clientValue.GetValue(), $"[{typeof(T)}] client child entity buffer value [{i}] expecting IS replicated when {sendForChildrenTestCase}"); + } + else + { + Assert.AreEqual(kDefaultValueIfNotReplicated, clientValue.GetValue(), $"[{typeof(T)}] client parent value [{i}] expecting NOT replicated when {sendForChildrenTestCase}"); } } } } - } - void VerifyLinkedComponentValues(int value, bool enabled, SendForChildrenTestCase sendForChildrenTestCase) + void VerifyLinkedComponentValues(int expectedValue, bool expectedEnabled, SendForChildrenTestCase sendForChildrenTestCase) where T : unmanaged, IComponentData, IEnableableComponent, IComponentValue { + VerifyLinkedComponentEnabled(expectedEnabled, sendForChildrenTestCase); + var type = ComponentType.ReadOnly(); var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(type); - using (var clientEntities = query.ToEntityArray(Allocator.TempJob)) { Assert.AreEqual(serverEntities.Length, clientEntities.Length); for (int i = 0; i < clientEntities.Length; i++) { - var serverEntity = serverEntities[i]; var clientEntity = clientEntities[i]; Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(clientEntity)); var clientEntityGroup = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntity); - Assert.AreEqual(2, clientEntityGroup.Length); - - var ent0 = testWorld.ClientWorlds[0].EntityManager - .IsComponentEnabled(clientEntityGroup[0].Value); - var ent1 = testWorld.ClientWorlds[0].EntityManager - .IsComponentEnabled(clientEntityGroup[1].Value); + Assert.AreEqual(2, clientEntityGroup.Length, "Client entity count should always be correct."); - Assert.True(enabled == ent0); - Assert.AreEqual(value, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntityGroup[0].Value).GetValue()); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + Assert.AreEqual(expectedValue, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntityGroup[0].Value).GetValue(), $"[{typeof(T)}] Expected that value on component on root entity [{i}] IS replicated correctly when using this `{sendForChildrenTestCase}`!"); + } + else + { + Assert.AreEqual(kDefaultValueIfNotReplicated, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntityGroup[1].Value).GetValue(), $"[{typeof(T)}] Expected that value on component on root entity [{i}] is NOT replicated by default (via this `{sendForChildrenTestCase}`)!"); + } - if (sendForChildrenTestCase != SendForChildrenTestCase.NoViaDontSerializeVariantDefault) + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, false)) { - Assert.True(enabled == ent1, $"Expected that the enable-bit on components on child entities ARE serialized when using `{sendForChildrenTestCase}`!"); - Assert.AreEqual(value, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntityGroup[1].Value).GetValue(), "Expected that value on component on child entities ARE serialized when using this `sendForChildrenTestCase`!"); + Assert.AreEqual(expectedValue, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntityGroup[1].Value).GetValue(), $"[{typeof(T)}] Expected that value on component on child entity [{i}] IS replicated when using this `{sendForChildrenTestCase}`!"); } else { - // TODO: Determine if enable-bits should be serialized when using `DontSerializeAttribute`. Sometimes they are? - //Assert.False(enabled == ent1, $"Expected that the enable-bit on components on child entities are NOT serialized by default (via this `{sendForChildrenTestCase}`)!"); - Assert.AreEqual(0, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntityGroup[1].Value).GetValue(), "Expected that value on components on child entities are not serialized by default (via this `sendForChildrenTestCase`)!"); + Assert.AreEqual(kDefaultValueIfNotReplicated, testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntityGroup[1].Value).GetValue(), $"[{typeof(T)}] Expected that value on component on child entity [{i}] is NOT replicated by default (via this `{sendForChildrenTestCase}`)!"); } } } } - void VerifyComponentValues(int value, bool enabled) where T: unmanaged, IComponentData, IEnableableComponent, IComponentValue + void VerifyLinkedComponentEnabled(bool expectedEnabled, SendForChildrenTestCase sendForChildrenTestCase) + where T : unmanaged, IComponentData, IEnableableComponent { - var type = ComponentType.ReadOnly(); + var type = ComponentType.ReadOnly(); var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(type); using (var clientEntities = query.ToEntityArray(Allocator.TempJob)) { - var totalEntities = query.CalculateEntityCountWithoutFiltering(); - Assert.True(clientEntities.Length != totalEntities ? !enabled : enabled); - Assert.AreEqual(serverEntities.Length, totalEntities); + Assert.AreEqual(serverEntities.Length, clientEntities.Length, $"[{typeof(T)}] Client has entities with this component."); for (int i = 0; i < clientEntities.Length; i++) { - var serverEntity = serverEntities[i]; var clientEntity = clientEntities[i]; - var isServerEnabled = testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntity); - var isClientEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntity); - Assert.AreEqual(enabled, isClientEnabled); - Assert.AreEqual(enabled, isServerEnabled); + Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(clientEntity), $"[{typeof(T)}] Client has entities with the LinkedEntityGroup."); - var serverValue = testWorld.ServerWorld.EntityManager.GetComponentData(serverEntity).GetValue(); - var clientValue = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntity).GetValue(); + var clientEntityGroup = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntity); + Assert.AreEqual(2, clientEntityGroup.Length, $"[{typeof(T)}] Entities in the LinkedEntityGroup!"); - Assert.AreEqual(value, serverValue); - Assert.AreEqual(value, clientValue); + var rootEntityEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntityGroup[0].Value); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + Assert.AreEqual(expectedEnabled, rootEntityEnabled, $"[{typeof(T)}] Expected that the enable-bit on component on root entity [{i}] is replicated when using `{sendForChildrenTestCase}`!"); + } + else + { + Assert.AreEqual(kDefaultIfNotReplicated, rootEntityEnabled, $"[{typeof(T)}] Expected that the enable-bit on component on root entity [{i}] is NOT replicated by default when using `{sendForChildrenTestCase}`!"); + } + + var childEntityEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntityGroup[1].Value); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, false)) + { + Assert.AreEqual(expectedEnabled, childEntityEnabled, $"[{typeof(T)}] Expected that the enable-bit on component on child entity [{i}] is replicated when using `{sendForChildrenTestCase}`!"); + } + else + { + Assert.AreEqual(kDefaultIfNotReplicated, childEntityEnabled, $"[{typeof(T)}] Expected that the enable-bit on component on child entity [{i}] is NOT replicated by default when using `{sendForChildrenTestCase}`!"); + } } } } - private NativeArray chunkArray; - - void CheckBufferValues(bool checkClients, bool enabledBit) - where T : struct, IBufferElementData, IEnableableComponent, IComponentValue + void VerifyComponentValues(int expectedServerValue, int expectedClientValue, bool expectedServerEnabled, bool expectedClientEnabled, SendForChildrenTestCase sendForChildrenTestCase) where T: unmanaged, IComponentData, IEnableableComponent, IComponentValue { - var serverType = ComponentType.ReadOnly(); - var serverQuery = testWorld.ServerWorld.EntityManager.CreateEntityQuery(serverType); - var entities = serverQuery.ToEntityArray(Allocator.TempJob); + var builder = new EntityQueryBuilder(Allocator.Temp) + .WithAll().WithOptions(EntityQueryOptions.IgnoreComponentEnabledState); + var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(builder); - int i = 0; - foreach (var entity in this.serverEntities) + using (var clientEntitiesWithoutFiltering = query.ToEntityArray(Allocator.TempJob)) { - var mgr = testWorld.ServerWorld.EntityManager; + Assert.AreEqual(serverEntities.Length, clientEntitiesWithoutFiltering.Length, "Client entity count must match server entity count!"); - if (chunkArray[i++] != mgr.GetChunk(entity)) - Debug.Log($"the chunk has changed"); + for (int i = 0; i < clientEntitiesWithoutFiltering.Length; i++) + { + var serverEntity = serverEntities[i]; + var clientEntity = clientEntitiesWithoutFiltering[i]; - var enabled = mgr.IsComponentEnabled(entity); - if (enabledBit != enabled) - Debug.Log($"component enabled is wrong. should be [{enabledBit}] but is [{enabled}]"); + var isServerEnabled = testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntity); + var isClientEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntity); + var serverValue = testWorld.ServerWorld.EntityManager.GetComponentData(serverEntity).GetValue(); + var clientValue = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEntity).GetValue(); + Assert.AreEqual(expectedServerEnabled, isServerEnabled, $"[{typeof(T)}] server enable bit [{i}]"); + Assert.AreEqual(expectedServerValue, serverValue, $"[{typeof(T)}] server value [{i}]"); + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + // Note that values are replicated even if the component is disabled! + Assert.AreEqual(expectedClientEnabled, isClientEnabled, $"[{typeof(T)}] client enable bit [{i}] IS replicated"); + Assert.AreEqual(expectedClientValue, clientValue, $"[{typeof(T)}] client value [{i}] IS replicated"); + } + else + { + Assert.AreEqual(kDefaultIfNotReplicated, isClientEnabled, $"[{typeof(T)}] client enable bit [{i}] NOT replicated"); + Assert.AreEqual(kDefaultValueIfNotReplicated, clientValue, $"[{typeof(T)}] client value [{i}] NOT replicated"); + } + } } + } - foreach (var serverEntity in entities) + void VerifyFlagComponentEnabledBit(bool expectedServerEnabled, bool expectedClientEnabled, SendForChildrenTestCase sendForChildrenTestCase) where T : unmanaged, IComponentData, IEnableableComponent + { + var type = ComponentType.ReadOnly(); + var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(type); + + using (var clientEntities = query.ToEntityArray(Allocator.TempJob)) { - if (serverEntity == Entity.Null) - Debug.Log($"query found a server entity that was NULL."); - } - if (!checkClients) - return; + var totalEntities = query.CalculateEntityCountWithoutFiltering(); + Assert.AreEqual(serverEntities.Length, totalEntities); - var clientType = ComponentType.ReadOnly(); - var clientQuery = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(clientType); - var clientEntities = clientQuery.ToEntityArray(Allocator.TempJob); + for (int i = 0; i < clientEntities.Length; i++) + { + var serverEntity = serverEntities[i]; + var clientEntity = clientEntities[i]; - foreach (var clientEntity in clientEntities) - { - if (clientEntity == Entity.Null) - Debug.Log($"found a server entity that was NULL."); + var isServerEnabled = testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntity); + var isClientEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntity); + Assert.AreEqual(expectedServerEnabled, isServerEnabled, $"Flag component {typeof(T)} server enabled bit is correct."); + + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + Assert.AreEqual(expectedClientEnabled, isClientEnabled, $"{typeof(T)} client enabled bit IS replicated."); + } + else + { + Assert.AreEqual(kDefaultIfNotReplicated, isClientEnabled, $"{typeof(T)} client enabled bit is NOT replicated."); + } + } } } - void VerifyBufferValues(int value, bool enabled) where T: unmanaged, IBufferElementData, IEnableableComponent, IComponentValue + NativeArray chunkArray; + + void VerifyBufferValues(int value, bool enabled, SendForChildrenTestCase sendForChildrenTestCase) where T: unmanaged, IBufferElementData, IEnableableComponent, IComponentValue { - var type = ComponentType.ReadOnly(); - var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(type); + var builder = new EntityQueryBuilder(Allocator.Temp).WithAll().WithOptions(EntityQueryOptions.IgnoreComponentEnabledState); + var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(builder); using (var clientEntities = query.ToEntityArray(Allocator.TempJob)) { var totalEntities = query.CalculateEntityCountWithoutFiltering(); - Assert.True(clientEntities.Length != totalEntities ? !enabled : enabled); - Assert.AreEqual(serverEntities.Length, totalEntities); + Assert.AreEqual(totalEntities, clientEntities.Length, $"Client entity count should ALWAYS be correct, regardless of setting: {sendForChildrenTestCase} and {typeof(T)}"); for (int i = 0; i < clientEntities.Length; i++) { @@ -479,23 +500,34 @@ void CheckBufferValues(bool checkClients, bool enabledBit) var isServerEnabled = testWorld.ServerWorld.EntityManager.IsComponentEnabled(serverEntity); var isClientEnabled = testWorld.ClientWorlds[0].EntityManager.IsComponentEnabled(clientEntity); - Assert.AreEqual(enabled, isClientEnabled); - Assert.AreEqual(enabled, isServerEnabled); - var serverBuffer = testWorld.ServerWorld.EntityManager.GetBuffer(serverEntity); var clientBuffer = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntity); - Assert.AreEqual(kBufferSize, serverBuffer.Length); - Assert.AreEqual(serverBuffer.Length, clientBuffer.Length); + Assert.AreEqual(kServerBufferSize, serverBuffer.Length, $"[{typeof(T)}] server buffer length"); + Assert.AreEqual(enabled, isServerEnabled, $"[{typeof(T)}] server enable bit"); + + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + { + Assert.AreEqual(enabled, isClientEnabled, $"[{typeof(T)}] Client enable bit IS replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(kServerBufferSize, clientBuffer.Length, $"[{typeof(T)}] Client buffer length IS replicated when {sendForChildrenTestCase}"); + } + else + { + Assert.AreEqual(kDefaultIfNotReplicated, isClientEnabled, $"[{typeof(T)}] Client enable bit is NOT replicated when {sendForChildrenTestCase}"); + Assert.AreEqual(kClientBufferSize, clientBuffer.Length, $"[{typeof(T)}] Client buffer length should NOT be replicated when {sendForChildrenTestCase}, thus should be the default CLIENT value"); + } for (int j = 0; j < serverBuffer.Length; ++j) { var serverValue = serverBuffer[j]; var clientValue = clientBuffer[j]; - var bufferValue = ((j + 1) * 1000 + value); - Assert.AreEqual(bufferValue, serverValue.GetValue()); - Assert.AreEqual(bufferValue, clientValue.GetValue()); + var expectedBufferValue = ((j + 1) * 1000 + value); + Assert.AreEqual(expectedBufferValue, serverValue.GetValue(), $"[{typeof(T)}] server buffer value [{i}]"); + + if (BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, true)) + Assert.AreEqual(expectedBufferValue, clientValue.GetValue(), $"[{typeof(T)}] client buffer value [{i}] IS replicated when {sendForChildrenTestCase}"); + else Assert.AreEqual(kDefaultValueIfNotReplicated, clientValue.GetValue(), $"[{typeof(T)}] client buffer value [{i}] is NOT replicated when {sendForChildrenTestCase}"); } } } @@ -508,6 +540,12 @@ void SetGhostValues(int value, bool enabled = false) { case GhostTypeConverter.GhostTypes.EnableableComponent: SetComponentValues(value, enabled); + SetComponentEnabled(enabled); + SetComponentValues(value, enabled); + SetComponentValues(value, enabled); + SetComponentValues(value, enabled); + SetComponentValues(value, enabled); + SetComponentEnabled(enabled); break; case GhostTypeConverter.GhostTypes.MultipleEnableableComponent: SetComponentValues(value, enabled); @@ -584,12 +622,24 @@ void SetGhostValues(int value, bool enabled = false) break; case GhostTypeConverter.GhostTypes.ChildComponent: SetLinkedComponentValues(value, enabled); + SetLinkedComponentEnabled(enabled); + SetLinkedComponentValues(value, enabled); + SetLinkedComponentValues(value, enabled); + SetLinkedComponentValues(value, enabled); + SetLinkedComponentValues(value, enabled); + SetLinkedComponentEnabled(enabled); break; case GhostTypeConverter.GhostTypes.ChildBufferComponent: SetLinkedBufferValues(value, enabled); break; case GhostTypeConverter.GhostTypes.GhostGroup: SetGhostGroupValues(value, enabled); + SetGhostGroupEnabled(enabled); + SetGhostGroupValues(value, enabled); + SetGhostGroupValues(value, enabled); + SetGhostGroupValues(value, enabled); + SetGhostGroupValues(value, enabled); + SetGhostGroupEnabled(enabled); break; default: Assert.True(true); @@ -597,189 +647,136 @@ void SetGhostValues(int value, bool enabled = false) } } - void CheckBuffers(bool checkClient, bool enableBit) - { - switch (type) - { - case GhostTypeConverter.GhostTypes.MultipleEnableableBuffer: - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - CheckBufferValues(checkClient, enableBit); - break; - default: - Assert.True(true); - break; - } - } void VerifyGhostValues(int value, bool enabled, SendForChildrenTestCase sendForChildrenTestCase) { Assert.IsTrue(serverEntities.IsCreated); switch (type) { case GhostTypeConverter.GhostTypes.EnableableComponent: - VerifyComponentValues(value, enabled); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyFlagComponentEnabledBit(enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, kDefaultIfNotReplicated, sendForChildrenTestCase); + VerifyComponentValues(value, kDefaultValueIfNotReplicated, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, kDefaultValueIfNotReplicated, enabled, kDefaultIfNotReplicated, sendForChildrenTestCase); + VerifyFlagComponentEnabledBit(enabled, kDefaultIfNotReplicated, sendForChildrenTestCase); break; case GhostTypeConverter.GhostTypes.MultipleEnableableComponent: - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); - VerifyComponentValues(value, enabled); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); + VerifyComponentValues(value, value, enabled, enabled, sendForChildrenTestCase); break; case GhostTypeConverter.GhostTypes.EnableableBuffer: - VerifyBufferValues(value, enabled); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); break; case GhostTypeConverter.GhostTypes.MultipleEnableableBuffer: - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); - VerifyBufferValues(value, enabled); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); + VerifyBufferValues(value, enabled, sendForChildrenTestCase); break; case GhostTypeConverter.GhostTypes.ChildComponent: VerifyLinkedComponentValues(value, enabled, sendForChildrenTestCase); + VerifyLinkedComponentEnabled(enabled, sendForChildrenTestCase); + VerifyLinkedComponentValues(value, kDefaultIfNotReplicated, sendForChildrenTestCase); + VerifyLinkedComponentValues(kDefaultValueIfNotReplicated, enabled, sendForChildrenTestCase); + // We override variants for these two, so cannot test their "default variants" without massive complications. + if (sendForChildrenTestCase != SendForChildrenTestCase.Default) + { + VerifyLinkedComponentValues(value, enabled, sendForChildrenTestCase); + VerifyLinkedComponentValues(kDefaultValueIfNotReplicated, kDefaultIfNotReplicated, sendForChildrenTestCase); + } + VerifyLinkedComponentEnabled(kDefaultIfNotReplicated, sendForChildrenTestCase); break; case GhostTypeConverter.GhostTypes.ChildBufferComponent: VerifyLinkedBufferValues(value, enabled, sendForChildrenTestCase); break; case GhostTypeConverter.GhostTypes.GhostGroup: - VerifyGhostGroupValues(value, enabled); + // GhostGroup implies all of these are root entities! I.e. No children to worry about, so `sendForChildrenTestCase` is ignored. + VerifyGhostGroupValues(value, enabled, sendForChildrenTestCase); + VerifyGhostGroupEnabledBits(enabled, sendForChildrenTestCase); + VerifyGhostGroupValues(value, kDefaultIfNotReplicated, sendForChildrenTestCase); + VerifyGhostGroupValues(kDefaultValueIfNotReplicated, enabled, sendForChildrenTestCase); + // We override variants for these two, so cannot test their "default variants" without massive complications. + if (sendForChildrenTestCase != SendForChildrenTestCase.Default) + { + VerifyGhostGroupValues(value, enabled, sendForChildrenTestCase); + VerifyGhostGroupEnabledBits(kDefaultIfNotReplicated, sendForChildrenTestCase); + } break; default: - Assert.True(true); + Assert.Fail(); break; } } - NetworkTick[] RetrieveLastTicks() - { - EntityQuery query = default; - var clientManager = testWorld.ClientWorlds[0].EntityManager; - if (this.type == GhostTypeConverter.GhostTypes.ChildBufferComponent || - this.type == GhostTypeConverter.GhostTypes.ChildComponent) - { - var type = ComponentType.ReadOnly(); - query = clientManager.CreateEntityQuery(type); - } - else - { - var type = ComponentType.ReadOnly(); - query = clientManager.CreateEntityQuery(type); - } - - if (query.IsEmpty) - return default; - - using (var clientEntities = query.ToEntityArray(Allocator.TempJob)) - { - Assert.AreEqual(serverEntities.Length, clientEntities.Length); - - var ticks = new NetworkTick[clientEntities.Length]; - - for (int i = 0; i < clientEntities.Length; i++) - { - var entity = clientEntities[i]; - var clientSnapshotBuffer = clientManager.GetBuffer(entity); - var clientSnapshot = clientManager.GetComponentData(entity); - var lastSnapshot = clientSnapshot.GetLatestTick(clientSnapshotBuffer); + const bool kDefaultIfNotReplicated = true; + const int kDefaultValueIfNotReplicated = 0; - ticks[i] = lastSnapshot; - } - - return ticks; - } - } + const int kServerBufferSize = 13; + internal const int kClientBufferSize = 29; - private const int kBufferSize = 16; - private NetCodeTestWorld testWorld; - private NativeArray serverEntities; - private GhostTypeConverter.GhostTypes type; + NetCodeTestWorld testWorld; + NativeArray serverEntities; + GhostTypeConverter.GhostTypes type; enum GhostFlags : int { @@ -790,8 +787,15 @@ enum GhostFlags : int void CreateWorldsAndSpawn(int numClients, GhostTypeConverter.GhostTypes type, int entityCount, GhostFlags flags, SendForChildrenTestCase sendForChildrenTestCase) { - if(sendForChildrenTestCase == SendForChildrenTestCase.YesViaDefaultNameDictionary) - testWorld.UserBakingSystems.Add(typeof(TestDefaultsToDefaultSerializationSystem)); + switch (sendForChildrenTestCase) + { + case SendForChildrenTestCase.YesViaDefaultVariantMap: + testWorld.UserBakingSystems.Add(typeof(ForceSerializeSystem)); + break; + case SendForChildrenTestCase.NoViaDefaultVariantMap: + testWorld.UserBakingSystems.Add(typeof(ForceDontSerializeSystem)); + break; + } testWorld.Bootstrap(true); @@ -918,16 +922,6 @@ public void TearDownTestsForEnableableBits() chunkArray.Dispose(); testWorld.Dispose(); } - private static void ValidateTicks(NetworkTick[] ticks, NetworkTick[] tickAfterAFewFrames, bool shouldBeEqual) - { - Assert.True(ticks.Length == tickAfterAFewFrames.Length); - for (int i = 0; i < ticks.Length; i++) - { - if (shouldBeEqual) - Assert.AreEqual(ticks[i], tickAfterAFewFrames[i], "Ticks should be same!"); - else Assert.AreNotEqual(ticks[i], tickAfterAFewFrames[i], "Ticks should NOT be same!"); - } - } [Test] public void GhostsAreSerializedWithEnabledBits([Values]GhostTypeConverter.GhostTypes type, [Values(1, 8)]int count, [Values]SendForChildrenTestCase sendForChildrenTestCase) @@ -938,8 +932,8 @@ public void GhostsAreSerializedWithEnabledBits([Values]GhostTypeConverter.GhostT var enabled = false; SetGhostValues(value, enabled); - var threshold = TickUntilReplicationIsDone(enabled); - VerifyGhostValues(value, enabled, sendForChildrenTestCase); + TickMultipleFrames(); + VerifyGhostValues(value, enabled, sendForChildrenTestCase); for (int i = 0; i < 8; ++i) { @@ -947,52 +941,55 @@ public void GhostsAreSerializedWithEnabledBits([Values]GhostTypeConverter.GhostT value = i; SetGhostValues(value, enabled); - TickMultipleFrames(threshold); + TickMultipleFrames(); VerifyGhostValues(value, enabled, sendForChildrenTestCase); } } [DisableAutoCreation] - class TestDefaultsToDefaultSerializationSystem : DefaultVariantSystemBase + class ForceSerializeSystem : DefaultVariantSystemBase + { + protected override void RegisterDefaultVariants(Dictionary defaultVariants) + { + var typesToOverride = GhostTypeConverter.FetchAllTestComponentTypesRequiringSendRuleOverride(); + foreach (var tuple in typesToOverride) + { + defaultVariants.Add(tuple.Item1, Rule.ForAll(tuple.Item2 ?? tuple.Item1)); + } + } + } + [DisableAutoCreation] + class ForceDontSerializeSystem : DefaultVariantSystemBase { protected override void RegisterDefaultVariants(Dictionary defaultVariants) { - var typesToOverride = FetchAllTestComponentTypes(GetType().Assembly); - foreach (var type in typesToOverride) + var typesToOverride = GhostTypeConverter.FetchAllTestComponentTypesRequiringSendRuleOverride(); + foreach (var tuple in typesToOverride) { - defaultVariants.Add(type, Rule.ForAll(type)); + defaultVariants.Add(tuple.Item1, Rule.ForAll(typeof(DontSerializeVariant))); } } } - GhostAuthoringInspectionComponent.ComponentOverride[] BuildComponentOverridesForComponents() + static GhostAuthoringInspectionComponent.ComponentOverride[] BuildComponentOverridesForComponents() { - var testTypes = FetchAllTestComponentTypes(GetType().Assembly); + var testTypes = GhostTypeConverter.FetchAllTestComponentTypesRequiringSendRuleOverride(); var overrides = testTypes .Select(x => { - var componentTypeFullName = x.FullName; + var componentTypeFullName = x.Item1.FullName; + var variantTypeName = x.Item2?.FullName ?? componentTypeFullName; return new GhostAuthoringInspectionComponent.ComponentOverride { FullTypeName = componentTypeFullName, PrefabType = GhostPrefabType.All, SendTypeOptimization = GhostSendType.AllClients, - VariantHash = GhostVariantsUtility.UncheckedVariantHashNBC(componentTypeFullName, componentTypeFullName), + VariantHash = GhostVariantsUtility.UncheckedVariantHashNBC(variantTypeName, componentTypeFullName), }; }).ToArray(); - - if (overrides.Length < 50) - throw new InvalidOperationException("There are loads of override types!"); return overrides; } - static Type[] FetchAllTestComponentTypes(Assembly assembly) - { - return assembly.GetTypes() - .Where(x => (typeof(IBufferElementData).IsAssignableFrom(x) && x.Name.StartsWith("EnableableBuffer")) || (typeof(IComponentData).IsAssignableFrom(x) && x.Name.StartsWith("EnableableComponent"))) - .ToArray(); - } - [Test] public void GhostsAreSerializedWithEnabledBits_PreSerialize([Values]GhostTypeConverter.GhostTypes type, [Values(1, 8)]int count, [Values]SendForChildrenTestCase sendForChildrenTestCase) { @@ -1002,7 +999,7 @@ public void GhostsAreSerializedWithEnabledBits_PreSerialize([Values]GhostTypeCon var enabled = false; SetGhostValues(value, enabled); - var threshold = TickUntilReplicationIsDone(enabled); + TickMultipleFrames(); VerifyGhostValues(value, enabled, sendForChildrenTestCase); for (int i = 0; i < 8; ++i) @@ -1011,7 +1008,7 @@ public void GhostsAreSerializedWithEnabledBits_PreSerialize([Values]GhostTypeCon value = i; SetGhostValues(value, enabled); - TickMultipleFrames(threshold); + TickMultipleFrames(); VerifyGhostValues(value, enabled, sendForChildrenTestCase); } } @@ -1033,33 +1030,26 @@ public void GhostsAreSerializedWithEnabledBits_PreSerialize([Values]GhostTypeCon var enabled = false; SetGhostValues(value, enabled); - var threshold = TickUntilReplicationIsDone(enabled); + TickMultipleFrames(); VerifyGhostValues(value, enabled, sendForChildrenTestCase); - var ticks = RetrieveLastTicks(); - - TickMultipleFrames(threshold); - ValidateTicks(ticks, RetrieveLastTicks(), true); + TickMultipleFrames(); value = 21; SetGhostValues(value, enabled); - TickMultipleFrames(threshold); + TickMultipleFrames(); VerifyGhostValues(value, enabled, sendForChildrenTestCase); - ValidateTicks(ticks, RetrieveLastTicks(), false); for (int i = 0; i < 8; ++i) { - ticks = RetrieveLastTicks(); enabled = !enabled; value = i; SetGhostValues(value, enabled); - TickMultipleFrames(threshold); + TickMultipleFrames(); VerifyGhostValues(value, enabled, sendForChildrenTestCase); - - ValidateTicks(ticks, RetrieveLastTicks(), false); } } } diff --git a/Tests/Editor/GhostSerializeBufferTests.cs b/Tests/Editor/GhostSerializeBufferTests.cs index fed759b..92b7930 100644 --- a/Tests/Editor/GhostSerializeBufferTests.cs +++ b/Tests/Editor/GhostSerializeBufferTests.cs @@ -587,7 +587,7 @@ public void BuffersSentInPartialChunkAreReceivedCorrectly() } [DisableAutoCreation] - class TestAddBufferToDefaultSerializationSystem : DefaultVariantSystemBase + class ForceSerializeBufferSystem : DefaultVariantSystemBase { protected override void RegisterDefaultVariants(Dictionary defaultVariants) { @@ -595,14 +595,29 @@ protected override void RegisterDefaultVariants(Dictionary } } + [DisableAutoCreation] + class ForceDontSerializeBufferSystem : DefaultVariantSystemBase + { + protected override void RegisterDefaultVariants(Dictionary defaultVariants) + { + defaultVariants.Add(typeof(GhostGenTest_Buffer), Rule.ForAll(typeof(DontSerializeVariant))); + } + } + [Test] public void ChildEntitiesBuffersAreSerializedCorrectly([Values]SendForChildrenTestCase sendForChildrenTestCase) { using (var testWorld = new NetCodeTestWorld()) { - if (sendForChildrenTestCase == SendForChildrenTestCase.YesViaDefaultNameDictionary) - testWorld.UserBakingSystems.Add(typeof(TestAddBufferToDefaultSerializationSystem)); - + switch (sendForChildrenTestCase) + { + case SendForChildrenTestCase.YesViaDefaultVariantMap: + testWorld.UserBakingSystems.Add(typeof(ForceSerializeBufferSystem)); + break; + case SendForChildrenTestCase.NoViaDefaultVariantMap: + testWorld.UserBakingSystems.Add(typeof(ForceDontSerializeBufferSystem)); + break; + } testWorld.Bootstrap(true); var ghostGameObject = new GameObject(); @@ -663,7 +678,7 @@ public void ChildEntitiesBuffersAreSerializedCorrectly([Values]SendForChildrenTe Assert.AreEqual(2, serverEntityGroup.Length); //Verify that the client snapshot data contains the right things - var shouldChildReceiveData = sendForChildrenTestCase != SendForChildrenTestCase.NoViaDontSerializeVariantDefault; + var shouldChildReceiveData = BootstrapTests.IsExpectedToBeReplicated(sendForChildrenTestCase, false); var dynamicBuffer = testWorld.ClientWorlds[0].EntityManager.GetBuffer(clientEntities[0]); if(shouldChildReceiveData) BufferTestHelper.ValidateMultiBufferSnapshotDataContents(dynamicBuffer, 3, 0, 10, 10); @@ -853,7 +868,7 @@ public partial class BufferTestPredictionSystem : SystemBase { protected override void OnUpdate() { - var tick = GetSingleton().ServerTick; + var tick = SystemAPI.GetSingleton().ServerTick; var deltaTime = SystemAPI.Time.DeltaTime; var bufferFromEntity = GetBufferLookup(); //FIXME: updating child entities is not efficient this way. @@ -1036,7 +1051,7 @@ protected override void OnDestroy() protected override void OnUpdate() { - var spawnListEntity = GetSingletonEntity(); + var spawnListEntity = SystemAPI.GetSingletonEntity(); var spawnListFromEntity = GetBufferLookup(); var predictedEntities = m_PredictedEntities; Entities @@ -1076,7 +1091,7 @@ public partial class SpawnPredictedGhost : SystemBase public Entity spawnedEntity; Entity SpawnPredictedEntity(int baseValue) { - var prefabsList = GetSingletonEntity(); + var prefabsList = SystemAPI.GetSingletonEntity(); var prefabs = EntityManager.GetBuffer(prefabsList); var entity = EntityManager.Instantiate(prefabs[0].Value); BufferTestHelper.SetByteBufferValues(World, entity, 5, baseValue); @@ -1084,10 +1099,10 @@ Entity SpawnPredictedEntity(int baseValue) } protected override void OnUpdate() { - var netTime = GetSingleton(); + var netTime = SystemAPI.GetSingleton(); if (spawnAtTick.IsValid && !spawnAtTick.IsNewerThan(NetworkTimeHelper.LastFullServerTick(netTime))) { - if(HasSingleton()) + if(SystemAPI.HasSingleton()) spawnedEntity = SpawnPredictedEntity(10); else spawnedEntity = SpawnPredictedEntity(100); diff --git a/Tests/Editor/InputComponentDataTest.cs b/Tests/Editor/InputComponentDataTest.cs index f10dc6f..0bcd119 100644 --- a/Tests/Editor/InputComponentDataTest.cs +++ b/Tests/Editor/InputComponentDataTest.cs @@ -129,18 +129,16 @@ public partial class GatherInputsSystem : SystemBase protected override void OnCreate() { RequireForUpdate(); - RequireForUpdate(); + RequireForUpdate(); } protected override void OnUpdate() { - var networkId = GetSingleton().Value; var didSetEvent = m_DidSetEvent; var waitTicks = m_WaitTicks; Entities - .ForEach((ref InputComponentData inputData, ref GhostOwnerComponent owner) => + .WithAll() + .ForEach((ref InputComponentData inputData) => { - if (owner.NetworkId != networkId) - return; inputData = default; inputData.Horizontal = 1; inputData.Vertical = 1; @@ -169,18 +167,16 @@ public partial class GatherInputsRemoteTestSystem : SystemBase protected override void OnCreate() { RequireForUpdate(); - RequireForUpdate(); + RequireForUpdate(); } protected override void OnUpdate() { - var networkId = GetSingleton().Value; + // Inputs are only gathered on the local player, so if any inputs are set on + // the remote player it's because they were fetched from the buffer (replicated via ghost system) Entities - .ForEach((ref InputRemoteTestComponentData inputData, ref GhostOwnerComponent owner) => + .WithAll() + .ForEach((ref InputRemoteTestComponentData inputData) => { - // Inputs are only gathered on the local player, so if any inputs are set on - // the remote player it's because they were fetched from the buffer (replicated via ghost system) - if (owner.NetworkId != networkId) - return; inputData = default; inputData.Horizontal = 1; inputData.Vertical = 1; @@ -200,7 +196,19 @@ protected override void OnUpdate() { var eventCounter = EventCounter; FixedString32Bytes world = World.Name; - var tick = GetSingleton().ServerTick; + var tick = SystemAPI.GetSingleton().ServerTick; +#if !ENABLE_TRANSFORM_V1 + Entities.WithAll().ForEach( + (ref InputComponentData input, ref LocalTransform trans) => + { + var newPosition = new float3(); + if (input.Jump.IsSet) + eventCounter++; + newPosition.x = input.Horizontal; + newPosition.z = input.Vertical; + trans = trans.WithPosition(newPosition); + }).Run(); +#else Entities.WithAll().ForEach( (ref InputComponentData input, ref Translation trans) => { @@ -211,6 +219,7 @@ protected override void OnUpdate() newPosition.z = input.Vertical; trans = new Translation() { Value = newPosition }; }).Run(); +#endif EventCounter = eventCounter; } } @@ -261,11 +270,19 @@ public void InputComponentData_IsCorrectlySynchronized() for (int i = 0; i < 16; ++i) testWorld.Tick(m_DeltaTime); +#if !ENABLE_TRANSFORM_V1 + // The IInputComponentData should have been copied to buffer, sent to server, and then transform + // result sent back to the client. + var transform = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt); + Assert.AreEqual(1f, transform.Position.x); + Assert.AreEqual(1f, transform.Position.z); +#else // The IInputComponentData should have been copied to buffer, sent to server, and then translation // result sent back to the client. var translation = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt); Assert.AreEqual(1f, translation.Value.x); Assert.AreEqual(1f, translation.Value.z); +#endif // Event should only fire once on the server (but can multiple times on client because of prediction loop) var serverInputSystem = testWorld.ServerWorld.GetExistingSystemManaged(); @@ -530,43 +547,41 @@ public void InputComponentData_BufferCopiesGhostComponentConfigFromInputComponen // synced in snapshots) but is just handled as a command is using var collectionQuery = testWorld.ServerWorld.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); var collectionData = collectionQuery.GetSingleton(); - var collection = collectionData.GhostComponentCollection.GetValueArray(Allocator.Temp); GhostComponentSerializer.State inputBufferWithGhostFieldsSerializerState = default; var inputBufferType = GetComponentType(testWorld.ServerWorld, nameof(InputComponentDataWithGhostComponent)); var inputBufferWithFieldsType = GetComponentType(testWorld.ServerWorld, nameof(InputComponentDataWithGhostComponentAndGhostFields)); - foreach (var state in collection) + foreach (var state in collectionData.Serializers) { if (state.ComponentType.CompareTo(inputBufferWithFieldsType) == 0) inputBufferWithGhostFieldsSerializerState = state; } - collection.Dispose(); // There should be empty variant configs for all the other types (input components and buffer without ghost fields) // where we'll find the prefab type registered - var emptyVariants = collectionData.EmptyVariants.GetValueArray(Allocator.Temp); var foundVariantForInputBuffer = false; var foundVariantForInputComponent = false; var foundVariantForInputComponentWithFields = false; - foreach (var variant in emptyVariants) + foreach (var nonSerializedStrategies in collectionData.SerializationStrategies) { - if (variant.Component.CompareTo(inputBufferType) == 0) + if (nonSerializedStrategies.IsSerialized != 0) + continue; + + if (nonSerializedStrategies.Component.CompareTo(inputBufferType) == 0) { foundVariantForInputBuffer = true; - Assert.AreEqual(GhostPrefabType.Client, variant.PrefabType); + Assert.AreEqual(GhostPrefabType.Client, nonSerializedStrategies.PrefabType); } - if (variant.Component.CompareTo(ComponentType.ReadWrite()) == 0) + if (nonSerializedStrategies.Component.CompareTo(ComponentType.ReadWrite()) == 0) { foundVariantForInputComponent = true; - Assert.AreEqual(GhostPrefabType.Client, variant.PrefabType); + Assert.AreEqual(GhostPrefabType.Client, nonSerializedStrategies.PrefabType); } - if (variant.Component.CompareTo(ComponentType.ReadWrite()) == 0) + if (nonSerializedStrategies.Component.CompareTo(ComponentType.ReadWrite()) == 0) { foundVariantForInputComponentWithFields = true; - Assert.AreEqual(GhostPrefabType.Client, variant.PrefabType); + Assert.AreEqual(GhostPrefabType.Client, nonSerializedStrategies.PrefabType); } } - emptyVariants.Dispose(); - Assert.IsTrue(foundVariantForInputBuffer); Assert.IsTrue(foundVariantForInputComponent); Assert.IsTrue(foundVariantForInputComponentWithFields); diff --git a/Tests/Editor/InterpolationTests.cs b/Tests/Editor/InterpolationTests.cs index 4802e0a..efb33f9 100644 --- a/Tests/Editor/InterpolationTests.cs +++ b/Tests/Editor/InterpolationTests.cs @@ -22,7 +22,11 @@ protected override void OnUpdate() { var deltaTime = SystemAPI.Time.DeltaTime; var speed = moveSpeed; +#if !ENABLE_TRANSFORM_V1 + Entities.ForEach((Entity ent, ref LocalTransform tx) => { tx.Position += new float3(speed * deltaTime); }).Run(); +#else Entities.ForEach((Entity ent, ref Translation tx) => { tx.Value += new float3(speed * deltaTime); }).Run(); +#endif } } @@ -38,11 +42,19 @@ protected override void OnUpdate() { Entities .WithoutBurst() +#if !ENABLE_TRANSFORM_V1 + .ForEach((Entity ent, in LocalTransform tx) => + { + Assert.GreaterOrEqual(tx.Position.x, prevPos.x); + prevPos = tx.Position; + }).Run(); +#else .ForEach((Entity ent, in Translation tx) => { Assert.GreaterOrEqual(tx.Value.x, prevPos.x); prevPos = tx.Value; }).Run(); +#endif } } diff --git a/Tests/Editor/InvalidUsageTests.cs b/Tests/Editor/InvalidUsageTests.cs index af658cf..366a4ba 100644 --- a/Tests/Editor/InvalidUsageTests.cs +++ b/Tests/Editor/InvalidUsageTests.cs @@ -35,7 +35,7 @@ protected override void OnUpdate() if (s_DeleteCount > 0) { --s_DeleteCount; - EntityManager.DestroyEntity(GetSingletonEntity()); + EntityManager.DestroyEntity(SystemAPI.GetSingletonEntity()); } } } diff --git a/Tests/Editor/PerPrefabOverridesTests.cs b/Tests/Editor/PerPrefabOverridesTests.cs index dadfd7c..d108dbe 100644 --- a/Tests/Editor/PerPrefabOverridesTests.cs +++ b/Tests/Editor/PerPrefabOverridesTests.cs @@ -460,12 +460,28 @@ public void OverrideComponentSendType_NestedChildEntity() } /// A client only variant we can assign. - [GhostComponentVariation(typeof(Transforms.Translation), "TranslationVariantTest", true)] +#if !ENABLE_TRANSFORM_V1 + [GhostComponentVariation(typeof(Transforms.LocalTransform), nameof(TransformVariantTest))] + [GhostComponent(PrefabType=GhostPrefabType.All, SendTypeOptimization=GhostSendType.AllClients)] + public struct TransformVariantTest + { + [GhostField(Quantization=100, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float3 Position; + + [GhostField(Quantization=100, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public float Scale; + + [GhostField(Quantization=1000, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] + public quaternion Rotation; + } +#else + [GhostComponentVariation(typeof(Transforms.Translation), nameof(TranslationVariantTest), true)] [GhostComponent(PrefabType = GhostPrefabType.All, SendTypeOptimization = GhostSendType.AllClients)] public struct TranslationVariantTest { [GhostField(Quantization=1000, Smoothing=SmoothingAction.Interpolate, SubType=0)] public float3 Value; } +#endif [Test] public void SerializationVariant_AreAppliedToBothRootAndChildEntities() @@ -485,25 +501,45 @@ public void SerializationVariant_AreAppliedToBothRootAndChildEntities() authoring.SupportedGhostModes = GhostModeMask.All; //Setup a variant for both root and child entity and check that the runtime serializer use this one to serialize data +#if !ENABLE_TRANSFORM_V1 + var attrType = typeof(TransformVariantTest).GetCustomAttribute(); +#else var attrType = typeof(TranslationVariantTest).GetCustomAttribute(); +#endif ulong hash = 0; using var collectionQuery = testWorld.ServerWorld.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); var collectionData = collectionQuery.GetSingleton(); - var serializers = collectionData.GhostComponentCollection.GetValueArray(Allocator.Temp); - foreach (var serializer in serializers) + foreach (var ssIndex in collectionData.SerializationStrategiesComponentTypeMap.GetValuesForKey(attrType.ComponentType)) { - if (serializer.ComponentType == attrType.ComponentType && GhostComponentSerializer.VariantTypes[serializer.VariantTypeIndex] == typeof(TranslationVariantTest)) - hash = serializer.VariantHash; + var ss = collectionData.SerializationStrategies[ssIndex]; +#if !ENABLE_TRANSFORM_V1 + if (ss.DisplayName.ToString().Contains(nameof(TransformVariantTest))) +#else + if (ss.DisplayName.ToString().Contains(nameof(TranslationVariantTest))) +#endif + { + hash = ss.Hash; + goto found; + } } - serializers.Dispose(); +#if !ENABLE_TRANSFORM_V1 + Assert.Fail($"Couldn't find {nameof(TransformVariantTest)} to apply it!"); +#else + Assert.Fail($"Couldn't find {nameof(TranslationVariantTest)} to apply it!"); +#endif + found: Assert.AreNotEqual(0, hash); inspection.ComponentOverrides = new[] { new GhostAuthoringInspectionComponent.ComponentOverride { +#if !ENABLE_TRANSFORM_V1 + FullTypeName = typeof(Transforms.LocalTransform).FullName, +#else FullTypeName = typeof(Transforms.Translation).FullName, +#endif GameObject = ghostGameObject, PrefabType = GhostPrefabType.All, SendTypeOptimization = GhostSendType.AllClients, @@ -514,7 +550,11 @@ public void SerializationVariant_AreAppliedToBothRootAndChildEntities() { new GhostAuthoringInspectionComponent.ComponentOverride { +#if !ENABLE_TRANSFORM_V1 + FullTypeName = typeof(Transforms.LocalTransform).FullName, +#else FullTypeName = typeof(Transforms.Translation).FullName, +#endif GameObject = childGhost, PrefabType = GhostPrefabType.All, SendTypeOptimization = GhostSendType.AllClients, @@ -525,7 +565,11 @@ public void SerializationVariant_AreAppliedToBothRootAndChildEntities() { new GhostAuthoringInspectionComponent.ComponentOverride { +#if !ENABLE_TRANSFORM_V1 + FullTypeName = typeof(Transforms.LocalTransform).FullName, +#else FullTypeName = typeof(Transforms.Translation).FullName, +#endif GameObject = nestedChildGhost, PrefabType = GhostPrefabType.All, SendTypeOptimization = GhostSendType.AllClients, @@ -549,7 +593,11 @@ public void SerializationVariant_AreAppliedToBothRootAndChildEntities() for(int i=0;i<16;++i) testWorld.Tick(1.0f/60.0f); +#if !ENABLE_TRANSFORM_V1 + var typeIndex = TypeManager.GetTypeIndex(); +#else var typeIndex = TypeManager.GetTypeIndex(); +#endif //Then check the expected results var collection = testWorld.TryGetSingletonEntity(testWorld.ServerWorld); var ghostSerializerCollection = testWorld.ServerWorld.EntityManager.GetBuffer(collection); diff --git a/Tests/Editor/Physics/HistoryBufferTests.cs b/Tests/Editor/Physics/HistoryBufferTests.cs index 2afc569..79cd9a5 100644 --- a/Tests/Editor/Physics/HistoryBufferTests.cs +++ b/Tests/Editor/Physics/HistoryBufferTests.cs @@ -22,8 +22,12 @@ protected override void OnUpdate() { var historyBuffer = new CollisionHistoryBuffer(1); Entities +#if !ENABLE_TRANSFORM_V1 + .WithAll() +#else .WithAll() - .ForEach((Entity entity, int entityInQueryIndex) => +#endif + .ForEach(() => { historyBuffer.GetCollisionWorldFromTick(new NetworkTick(0),0, out var world); }).Schedule(); @@ -33,10 +37,14 @@ protected override void OnUpdate() }, "PhysicHistoryBuffer must be declared as ReadOnly if a job does not write to it"); Entities +#if !ENABLE_TRANSFORM_V1 + .WithAll() +#else .WithAll() +#endif .WithoutBurst() //.WithReadOnly(historyBuffer) - .ForEach((Entity entity, int entityInQueryIndex) => + .ForEach(() => { historyBuffer.GetCollisionWorldFromTick(new NetworkTick(0),0, out var world); }).Schedule(); @@ -315,7 +323,11 @@ public void PhysicsHistoryBuffer_SupportEntityForEach() { var entityWorld = new World("NetCodeTest"); entityWorld.GetOrCreateSystemManaged(); +#if !ENABLE_TRANSFORM_V1 + var archetype = entityWorld.EntityManager.CreateArchetype(typeof(LocalTransform)); +#else var archetype = entityWorld.EntityManager.CreateArchetype(typeof(Translation)); +#endif var entities = entityWorld.EntityManager.CreateEntity(archetype, 10, Allocator.Temp); entities.Dispose(); Assert.DoesNotThrow(() => { entityWorld.Update(); }); diff --git a/Tests/Editor/Physics/LagCompensationTests.cs b/Tests/Editor/Physics/LagCompensationTests.cs index 75b1594..fdfbb59 100644 --- a/Tests/Editor/Physics/LagCompensationTests.cs +++ b/Tests/Editor/Physics/LagCompensationTests.cs @@ -175,11 +175,19 @@ public partial class LagCompensationTestCubeMoveSystem : SystemBase { protected override void OnUpdate() { +#if !ENABLE_TRANSFORM_V1 + Entities.WithNone().WithAll().ForEach((ref LocalTransform trans) => { + trans.Position.x += 0.1f; + if (trans.Position.x > 100) + trans.Position.x -= 200; + }).ScheduleParallel(); +#else Entities.WithNone().WithAll().ForEach((ref Translation pos) => { pos.Value.x += 0.1f; if (pos.Value.x > 100) pos.Value.x -= 200; }).ScheduleParallel(); +#endif } } @@ -193,12 +201,12 @@ public partial class LagCompensationTestHitScanSystem : SystemBase public static bool EnableLagCompensation = true; protected override void OnUpdate() { - var networkTime = GetSingleton(); + var networkTime = SystemAPI.GetSingleton(); // Do not perform hit-scan when rolling back, only when simulating the latest tick if (!networkTime.IsFirstTimeFullyPredictingTick) return; - var collisionHistory = GetSingleton(); - var physicsWorld = GetSingleton().PhysicsWorld; + var collisionHistory = SystemAPI.GetSingleton(); + var physicsWorld = SystemAPI.GetSingleton().PhysicsWorld; var predictingTick = networkTime.ServerTick; var isServer = World.IsServer(); // Not using burst since there is a static used to update the UI @@ -242,14 +250,15 @@ protected override void OnCreate() } protected override void OnUpdate() { - var target = GetSingleton(); - var networkTime = GetSingleton(); + var target = SystemAPI.GetSingleton(); + var networkTime = SystemAPI.GetSingleton(); if (target.targetEntity == Entity.Null) { - Entities.WithoutBurst().WithAll().ForEach((Entity entity, in LagCompensationTestPlayer player) => { + foreach (var (ghost, entity) in SystemAPI.Query>().WithEntityAccess().WithAll()) + { target.targetEntity = entity; - SetSingleton(target); - }).Run(); + SystemAPI.SetSingleton(target); + } } if (target.targetEntity == Entity.Null || !networkTime.ServerTick.IsValid || !EntityManager.HasComponent(target.targetEntity)) return; @@ -259,12 +268,21 @@ protected override void OnUpdate() cmd.Tick = networkTime.ServerTick; if (math.any(Target != default)) { +#if !ENABLE_TRANSFORM_V1 + Entities.WithoutBurst().WithNone().WithAll().ForEach((in LocalTransform trans) => { + var offset = new float3(0,0,-10); + cmd.origin = trans.Position + offset; + cmd.direction = Target - offset; + cmd.lastFire = cmd.Tick; + }).Run(); +#else Entities.WithoutBurst().WithNone().WithAll().ForEach((in Translation pos) => { var offset = new float3(0,0,-10); cmd.origin = pos.Value + offset; cmd.direction = Target - offset; cmd.lastFire = cmd.Tick; }).Run(); +#endif // If too close to an edge, wait a bit if (cmd.origin.x < -90 || cmd.origin.x > 90) { diff --git a/Tests/Editor/PredictionSwitchTests.cs b/Tests/Editor/PredictionSwitchTests.cs index 5614ce0..f114408 100644 --- a/Tests/Editor/PredictionSwitchTests.cs +++ b/Tests/Editor/PredictionSwitchTests.cs @@ -32,6 +32,28 @@ public struct InterpolatedOnlyTestComponent : IComponentData { public int Value; } + [DisableAutoCreation] + [UpdateInGroup(typeof(PredictedSimulationSystemGroup))] + internal partial class PredictionSwitchMoveTestSystem : SystemBase + { + protected override void OnUpdate() + { + // Only update position every second tick + if ((SystemAPI.GetSingleton().ServerTick.TickIndexForValidTick&1u) == 0) + return; +#if !ENABLE_TRANSFORM_V1 + foreach (var trans in SystemAPI.Query>().WithAll().WithAll()) + { + trans.ValueRW.Position += new float3(1, 0, 0); + } +#else + foreach (var trans in SystemAPI.Query>().WithAll().WithAll()) + { + trans.ValueRW.Value += new float3(1, 0, 0); + } +#endif + } + } public class PredictionSwitchTests { const float frameTime = 1.0f / 60.0f; @@ -102,5 +124,69 @@ public void SwitchingPredictionAddsAndRemovesComponent() Assert.AreEqual(43, entityManager.GetComponentData(clientEnt).Value); } } + + [Test] + public void SwitchingPredictionSmoothChildEntities() + { + using (var testWorld = new NetCodeTestWorld()) + { + testWorld.Bootstrap(true, typeof(PredictionSwitchMoveTestSystem)); + + var ghostGameObject = new GameObject(); + var childGameObject = new GameObject(); + + childGameObject.transform.parent = ghostGameObject.transform; + + ghostGameObject.AddComponent().Converter = new PredictionSwitchTestConverter(); + var ghostConfig = ghostGameObject.AddComponent(); + + Assert.IsTrue(testWorld.CreateGhostCollection(ghostGameObject)); + + testWorld.CreateWorlds(true, 1); + + var serverEnt = testWorld.SpawnOnServer(ghostGameObject); + Assert.AreNotEqual(Entity.Null, serverEnt); + + // Connect and make sure the connection could be established + Assert.IsTrue(testWorld.Connect(frameTime, 4)); + + // Go in-game + testWorld.GoInGame(); + + // Let the game run for a bit so the ghosts are spawned on the client + for (int i = 0; i < 16; ++i) + testWorld.Tick(frameTime); + + var firstClientWorld = testWorld.ClientWorlds[0]; + var clientEnt = testWorld.TryGetSingletonEntity(firstClientWorld); + Assert.AreNotEqual(Entity.Null, clientEnt); + + // Validate that the entity is interpolated + var entityManager = firstClientWorld.EntityManager; + ref var ghostPredictionSwitchingQueues = ref testWorld.GetSingletonRW(firstClientWorld).ValueRW; + + var childEnt = entityManager.GetBuffer(clientEnt)[1].Value; + Assert.AreNotEqual(Entity.Null, childEnt); + ghostPredictionSwitchingQueues.ConvertToPredictedQueue.Enqueue(new ConvertPredictionEntry + { + TargetEntity = clientEnt, + TransitionDurationSeconds = 1f, + }); + testWorld.Tick(frameTime); + + // validate that the position updates every frame and that the child and parent entity has identical LocalToWorld + var localToWorld = entityManager.GetComponentData(clientEnt); + for (int i = 0; i < 32; ++i) + { + testWorld.Tick(frameTime); + var nextLocalToWorld = entityManager.GetComponentData(clientEnt); + Assert.AreNotEqual(localToWorld.Value, nextLocalToWorld.Value); + var childLocalToWorld = entityManager.GetComponentData(childEnt); + Assert.AreEqual(nextLocalToWorld.Value, childLocalToWorld.Value); + + localToWorld = nextLocalToWorld; + } + } + } } } diff --git a/Tests/Editor/PredictionTests.cs b/Tests/Editor/PredictionTests.cs index f29329f..441d045 100644 --- a/Tests/Editor/PredictionTests.cs +++ b/Tests/Editor/PredictionTests.cs @@ -26,10 +26,17 @@ protected override void OnUpdate() if (!s_IsEnabled) return; var deltaTime = SystemAPI.Time.DeltaTime; +#if !ENABLE_TRANSFORM_V1 + Entities.WithAll().ForEach((ref LocalTransform trans) => { + // Make sure we advance by one unit per tick, makes it easier to debug the values + trans.Position.x += deltaTime * 60.0f; + }).ScheduleParallel(); +#else Entities.WithAll().ForEach((ref Translation trans) => { // Make sure we advance by one unit per tick, makes it easier to debug the values trans.Value.x += deltaTime * 60.0f; }).ScheduleParallel(); +#endif } } public class PredictionTests @@ -96,11 +103,26 @@ public void PartialPredictionTicksAreRolledBack() var clientEnt = testWorld.TryGetSingletonEntity(testWorld.ClientWorlds[0]); Assert.AreNotEqual(Entity.Null, clientEnt); +#if !ENABLE_TRANSFORM_V1 + var prevServer = testWorld.ServerWorld.EntityManager.GetComponentData(serverEnt).Position; + var prevClient = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt).Position; +#else var prevServer = testWorld.ServerWorld.EntityManager.GetComponentData(serverEnt).Value; var prevClient = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt).Value; +#endif for (int i = 0; i < 64; ++i) { testWorld.Tick(frameTime / 4); +#if !ENABLE_TRANSFORM_V1 + var curServer = testWorld.ServerWorld.EntityManager.GetComponentData(serverEnt); + var curClient = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt); + // Server does not do fractional ticks so it will not advance the position every frame + Assert.GreaterOrEqual(curServer.Position.x, prevServer.x); + // Client does fractional ticks and position should be always increasing + Assert.Greater(curClient.Position.x, prevClient.x); + prevServer = curServer.Position; + prevClient = curClient.Position; +#else var curServer = testWorld.ServerWorld.EntityManager.GetComponentData(serverEnt).Value; var curClient = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt).Value; // Server does not do fractional ticks so it will not advance the position every frame @@ -109,6 +131,7 @@ public void PartialPredictionTicksAreRolledBack() Assert.Greater(curClient.x, prevClient.x); prevServer = curServer; prevClient = curClient; +#endif } // Stop updating, let it run for a while and check that they ended on the same value @@ -116,8 +139,13 @@ public void PartialPredictionTicksAreRolledBack() for (int i = 0; i < 16; ++i) testWorld.Tick(frameTime); +#if !ENABLE_TRANSFORM_V1 + prevServer = testWorld.ServerWorld.EntityManager.GetComponentData(serverEnt).Position; + prevClient = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt).Position; +#else prevServer = testWorld.ServerWorld.EntityManager.GetComponentData(serverEnt).Value; prevClient = testWorld.ClientWorlds[0].EntityManager.GetComponentData(clientEnt).Value; +#endif Assert.IsTrue(math.distance(prevServer, prevClient) < 0.01); } } diff --git a/Tests/Editor/Prespawn/Assets.meta b/Tests/Editor/Prespawn/Assets.meta new file mode 100644 index 0000000..f114d57 --- /dev/null +++ b/Tests/Editor/Prespawn/Assets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fab94ad2c81494e94b1e651765d18964 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A Variant.prefab b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A Variant.prefab new file mode 100644 index 0000000..03b3add --- /dev/null +++ b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A Variant.prefab @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1001 &4688622179325442374 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 63904447334045665, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_Name + value: Whitebox_Ground_1600x1600_A Variant Variant + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_RootOrder + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalPosition.x + value: -0.29740465 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalPosition.z + value: -71.48201 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalRotation.w + value: 0.7071068 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalRotation.x + value: -0.7071068 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3942990643550168562, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5123344914533893751, guid: 1049df3aa2892401e94cc5e6f2d6132e, + type: 3} + propertyPath: prefabId + value: 395c18b533ba44ed9a0e3e8fe336d62f + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 1049df3aa2892401e94cc5e6f2d6132e, type: 3} diff --git a/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A Variant.prefab.meta b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A Variant.prefab.meta new file mode 100644 index 0000000..faeb75b --- /dev/null +++ b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A Variant.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f86c6a4a43afb4565b4f990e97fe650f +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.FBX b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.FBX new file mode 100644 index 0000000..4ebeeaa --- /dev/null +++ b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.FBX @@ -0,0 +1,320 @@ +; FBX 7.5.0 project file +; ---------------------------------------------------- + +FBXHeaderExtension: { + FBXHeaderVersion: 1003 + FBXVersion: 7500 + CreationTimeStamp: { + Version: 1000 + Year: 2019 + Month: 8 + Day: 15 + Hour: 11 + Minute: 5 + Second: 16 + Millisecond: 681 + } + Creator: "FBX SDK/FBX Plugins version 2018.1" + SceneInfo: "SceneInfo::GlobalInfo", "UserData" { + Type: "UserData" + Version: 100 + MetaData: { + Version: 100 + Title: "" + Subject: "" + Author: "" + Keywords: "" + Revision: "" + Comment: "" + } + Properties70: { + P: "DocumentUrl", "KString", "Url", "", "C:\P4\a2-dots-shooter\dev\ProjectFolder\Assets\_Test\Unite_CPH_Whitebox\Models\Whitebox_Shapes_A\Whitebox_Ground_1600x1600_A.FBX" + P: "SrcDocumentUrl", "KString", "Url", "", "C:\P4\a2-dots-shooter\dev\ProjectFolder\Assets\_Test\Unite_CPH_Whitebox\Models\Whitebox_Shapes_A\Whitebox_Ground_1600x1600_A.FBX" + P: "Original", "Compound", "", "" + P: "Original|ApplicationVendor", "KString", "", "", "Autodesk" + P: "Original|ApplicationName", "KString", "", "", "3ds Max" + P: "Original|ApplicationVersion", "KString", "", "", "2018" + P: "Original|DateTime_GMT", "DateTime", "", "", "15/08/2019 09:05:16.678" + P: "Original|FileName", "KString", "", "", "C:\P4\a2-dots-shooter\dev\ProjectFolder\Assets\_Test\Unite_CPH_Whitebox\Models\Whitebox_Shapes_A\Whitebox_Ground_1600x1600_A.FBX" + P: "LastSaved", "Compound", "", "" + P: "LastSaved|ApplicationVendor", "KString", "", "", "Autodesk" + P: "LastSaved|ApplicationName", "KString", "", "", "3ds Max" + P: "LastSaved|ApplicationVersion", "KString", "", "", "2018" + P: "LastSaved|DateTime_GMT", "DateTime", "", "", "15/08/2019 09:05:16.678" + P: "Original|ApplicationActiveProject", "KString", "", "", "C:\Users\janusk\Documents\3dsMax" + P: "Original|ApplicationNativeFile", "KString", "", "", "C:\P4\a2-dots-shooter\dev\ProjectFolder\SourceAssets\_Test\Unite_CPH_Whitebox\Models\Whitebox_Shapes_A\Whitebox_Shapes_A.max" + } + } +} +GlobalSettings: { + Version: 1000 + Properties70: { + P: "UpAxis", "int", "Integer", "",2 + P: "UpAxisSign", "int", "Integer", "",1 + P: "FrontAxis", "int", "Integer", "",1 + P: "FrontAxisSign", "int", "Integer", "",-1 + P: "CoordAxis", "int", "Integer", "",0 + P: "CoordAxisSign", "int", "Integer", "",1 + P: "OriginalUpAxis", "int", "Integer", "",2 + P: "OriginalUpAxisSign", "int", "Integer", "",1 + P: "UnitScaleFactor", "double", "Number", "",1 + P: "OriginalUnitScaleFactor", "double", "Number", "",1 + P: "AmbientColor", "ColorRGB", "Color", "",0,0,0 + P: "DefaultCamera", "KString", "", "", "Producer Perspective" + P: "TimeMode", "enum", "", "",6 + P: "TimeProtocol", "enum", "", "",2 + P: "SnapOnFrameMode", "enum", "", "",0 + P: "TimeSpanStart", "KTime", "Time", "",0 + P: "TimeSpanStop", "KTime", "Time", "",153953860000 + P: "CustomFrameRate", "double", "Number", "",-1 + P: "TimeMarker", "Compound", "", "" + P: "CurrentTimeMarker", "int", "Integer", "",-1 + } +} + +; Documents Description +;------------------------------------------------------------------ + +Documents: { + Count: 1 + Document: 1385848324368, "", "Scene" { + Properties70: { + P: "SourceObject", "object", "", "" + P: "ActiveAnimStackName", "KString", "", "", "" + } + RootNode: 0 + } +} + +; Document References +;------------------------------------------------------------------ + +References: { +} + +; Object definitions +;------------------------------------------------------------------ + +Definitions: { + Version: 100 + Count: 5 + ObjectType: "GlobalSettings" { + Count: 1 + } + ObjectType: "Model" { + Count: 2 + PropertyTemplate: "FbxNode" { + Properties70: { + P: "QuaternionInterpolate", "enum", "", "",0 + P: "RotationOffset", "Vector3D", "Vector", "",0,0,0 + P: "RotationPivot", "Vector3D", "Vector", "",0,0,0 + P: "ScalingOffset", "Vector3D", "Vector", "",0,0,0 + P: "ScalingPivot", "Vector3D", "Vector", "",0,0,0 + P: "TranslationActive", "bool", "", "",0 + P: "TranslationMin", "Vector3D", "Vector", "",0,0,0 + P: "TranslationMax", "Vector3D", "Vector", "",0,0,0 + P: "TranslationMinX", "bool", "", "",0 + P: "TranslationMinY", "bool", "", "",0 + P: "TranslationMinZ", "bool", "", "",0 + P: "TranslationMaxX", "bool", "", "",0 + P: "TranslationMaxY", "bool", "", "",0 + P: "TranslationMaxZ", "bool", "", "",0 + P: "RotationOrder", "enum", "", "",0 + P: "RotationSpaceForLimitOnly", "bool", "", "",0 + P: "RotationStiffnessX", "double", "Number", "",0 + P: "RotationStiffnessY", "double", "Number", "",0 + P: "RotationStiffnessZ", "double", "Number", "",0 + P: "AxisLen", "double", "Number", "",10 + P: "PreRotation", "Vector3D", "Vector", "",0,0,0 + P: "PostRotation", "Vector3D", "Vector", "",0,0,0 + P: "RotationActive", "bool", "", "",0 + P: "RotationMin", "Vector3D", "Vector", "",0,0,0 + P: "RotationMax", "Vector3D", "Vector", "",0,0,0 + P: "RotationMinX", "bool", "", "",0 + P: "RotationMinY", "bool", "", "",0 + P: "RotationMinZ", "bool", "", "",0 + P: "RotationMaxX", "bool", "", "",0 + P: "RotationMaxY", "bool", "", "",0 + P: "RotationMaxZ", "bool", "", "",0 + P: "InheritType", "enum", "", "",0 + P: "ScalingActive", "bool", "", "",0 + P: "ScalingMin", "Vector3D", "Vector", "",0,0,0 + P: "ScalingMax", "Vector3D", "Vector", "",1,1,1 + P: "ScalingMinX", "bool", "", "",0 + P: "ScalingMinY", "bool", "", "",0 + P: "ScalingMinZ", "bool", "", "",0 + P: "ScalingMaxX", "bool", "", "",0 + P: "ScalingMaxY", "bool", "", "",0 + P: "ScalingMaxZ", "bool", "", "",0 + P: "GeometricTranslation", "Vector3D", "Vector", "",0,0,0 + P: "GeometricRotation", "Vector3D", "Vector", "",0,0,0 + P: "GeometricScaling", "Vector3D", "Vector", "",1,1,1 + P: "MinDampRangeX", "double", "Number", "",0 + P: "MinDampRangeY", "double", "Number", "",0 + P: "MinDampRangeZ", "double", "Number", "",0 + P: "MaxDampRangeX", "double", "Number", "",0 + P: "MaxDampRangeY", "double", "Number", "",0 + P: "MaxDampRangeZ", "double", "Number", "",0 + P: "MinDampStrengthX", "double", "Number", "",0 + P: "MinDampStrengthY", "double", "Number", "",0 + P: "MinDampStrengthZ", "double", "Number", "",0 + P: "MaxDampStrengthX", "double", "Number", "",0 + P: "MaxDampStrengthY", "double", "Number", "",0 + P: "MaxDampStrengthZ", "double", "Number", "",0 + P: "PreferedAngleX", "double", "Number", "",0 + P: "PreferedAngleY", "double", "Number", "",0 + P: "PreferedAngleZ", "double", "Number", "",0 + P: "LookAtProperty", "object", "", "" + P: "UpVectorProperty", "object", "", "" + P: "Show", "bool", "", "",1 + P: "NegativePercentShapeSupport", "bool", "", "",1 + P: "DefaultAttributeIndex", "int", "Integer", "",-1 + P: "Freeze", "bool", "", "",0 + P: "LODBox", "bool", "", "",0 + P: "Lcl Translation", "Lcl Translation", "", "A",0,0,0 + P: "Lcl Rotation", "Lcl Rotation", "", "A",0,0,0 + P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1 + P: "Visibility", "Visibility", "", "A",1 + P: "Visibility Inheritance", "Visibility Inheritance", "", "",1 + } + } + } + ObjectType: "NodeAttribute" { + Count: 1 + PropertyTemplate: "FbxNull" { + Properties70: { + P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8 + P: "Size", "double", "Number", "",100 + P: "Look", "enum", "", "",1 + } + } + } + ObjectType: "Geometry" { + Count: 1 + PropertyTemplate: "FbxMesh" { + Properties70: { + P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8 + P: "BBoxMin", "Vector3D", "Vector", "",0,0,0 + P: "BBoxMax", "Vector3D", "Vector", "",0,0,0 + P: "Primary Visibility", "bool", "", "",1 + P: "Casts Shadows", "bool", "", "",1 + P: "Receive Shadows", "bool", "", "",1 + } + } + } +} + +; Object properties +;------------------------------------------------------------------ + +Objects: { + NodeAttribute: 1384966798384, "NodeAttribute::", "Null" { + TypeFlags: "Null" + } + Geometry: 1385635998432, "Geometry::", "Mesh" { + Properties70: { + P: "Color", "ColorRGB", "Color", "",0.109803921568627,0.349019607843137,0.694117647058824 + } + Vertices: *123 { + a: -400,-800,0,0,-800,0,400,-800,0,-800,-400,0,-400,-400,0,0,-400,0,400,-400,0,800,-400,0,-800,0,0,-400,0,0,0,0,0,400,0,0,800,0,0,-800,400,0,-400,400,0,0,400,0,400,400,0,800,400,0,-400,800,0,0,800,0,400,800,0,-780.000122070313,-400,0,-780.000122070313,0,0,-780.000122070313,400,0,-800,800,0,779.999755859375,-400,0,800,-800,0,779.999755859375,0,0,779.999755859375,400,0,-800,-800,0,-400,-780,0,0,-780,0,400,-780,0,-780.000122070313,-780,0,779.999755859375,-780,0,-400,780,0,0,780,0,400,780,0,800,800,0,-780.000122070313,780,0,779.999755859375,780,0 + } + PolygonVertexIndex: *128 { + a: 29,33,21,-4,30,31,5,-5,31,32,6,-6,32,34,25,-7,3,21,22,-9,4,5,10,-10,5,6,11,-11,6,25,27,-12,8,22,23,-14,9,10,15,-15,10,11,16,-16,11,27,28,-17,35,36,19,-19,36,37,20,-20,37,40,38,-21,0,30,33,-30,4,9,22,-22,9,14,23,-23,14,35,39,-24,7,12,27,-26,12,17,28,-28,17,38,40,-29,0,1,31,-31,1,2,32,-32,2,26,34,-33,4,21,33,-31,7,25,34,-27,13,23,39,-25,14,15,36,-36,15,16,37,-37,16,28,40,-38,18,24,39,-36 + } + Edges: *72 { + a: 3,100,60,6,89,88,10,93,92,104,96,19,65,64,22,21,26,25,77,76,35,69,68,38,37,42,41,81,80,124,72,50,113,54,117,84,2,63,18,34,0,17,33,109,14,30,46,58,29,45,121,103,5,9,107,1,13,4,8,12,61,97,111,127,49,53,57,125,48,52,56,73 + } + GeometryVersion: 124 + LayerElementNormal: 0 { + Version: 102 + Name: "" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "Direct" + Normals: *384 { + a: 0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0.999999940395355,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0.999999940395355,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1 + } + NormalsW: *128 { + a: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + } + } + LayerElementUV: 0 { + Version: 101 + Name: "UVChannel_1" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "IndexToDirect" + UV: *130 { + a: 0.524999499320984,1.89010155200958,0.524999737739563,7.62063932418823,0.999999761581421,7.52619647979736,0.999999523162842,1.89010107517242,-3.40520637109876e-06,-3.40526457875967e-06,0.999999701976776,5.73178148269653,0.999999642372131,3.84292483329773,0,-3.40526457875967e-06,0.999999523162842,1.95406770706177,3.40520637109876e-06,-3.40526457875967e-06,0.999999523162842,0.159654438495636,0.999999701976776,5.78063106536865,0.524999499320984,3.77895903587341,0.999999642372131,3.77895832061768,-3.40520637109876e-06,-5.82076609134674e-11,0,-5.82076609134674e-11,3.40520637109876e-06,-5.82076609134674e-11,0.999999642372131,3.89177441596985,0.524999737739563,5.66781568527222,0.999999701976776,5.66781520843506,-3.40520637109876e-06,3.40514816343784e-06,0,3.40514816343784e-06,3.40520637109876e-06,3.40514816343784e-06,0.999999523162842,2.00291681289673,0.524999499320984,1.98595595359802,0.999999523162842,1.98595523834229,0.999999642372131,3.87481188774109,0.524999499320984,3.87481236457825,0.999999701976776,5.76366949081421,0.524999737739563,5.76367044448853,0.999999761581421,7.55808258056641,0.524999737739563,7.65252780914307,0.524999737739563,5.73178195953369,0.999999523162842,0.191541522741318,0.524999737739563,5.78063154220581,0.524999499320984,3.89177441596985,0.524999499320984,2.00291752815247,0.524999499320984,3.84292507171631,0.524999499320984,1.9540684223175,0.524999499320984,0.0652110055088997,0.524999499320984,0.0970990061759949,-6.64009712636471e-06,-6.64015533402562e-06,-6.64009712636471e-06,-3.40526457875967e-06,-3.40520637109876e-06,-6.64015533402562e-06,0,-6.64015533402562e-06,3.40520637109876e-06,-6.64015533402562e-06,6.64009712636471e-06,-6.64015533402562e-06,6.64009712636471e-06,-3.40526457875967e-06,-6.64009712636471e-06,-5.82076609134674e-11,6.64009712636471e-06,-5.82076609134674e-11,-6.64009712636471e-06,3.40514816343784e-06,6.64009712636471e-06,3.40514816343784e-06,-3.40520637109876e-06,6.64009712636471e-06,0,6.64009712636471e-06,3.40520637109876e-06,6.64009712636471e-06,6.64009712636471e-06, +6.64009712636471e-06,-6.64009712636471e-06,6.64009712636471e-06,0.524999499320984,0.00124386232346296,0.999999523162842,0.0956868454813957,0.999999761581421,7.57504558563232,0.999999523162842,0.208503112196922,0.524999499320984,0.114060133695602,0.999999761581421,7.46222877502441,0.524999737739563,7.66948890686035,0.524999737739563,7.5566725730896 + } + UVIndex: *128 { + a: 57,58,3,0,43,44,7,4,44,45,9,7,45,46,47,9,0,3,13,12,4,7,15,14,7,9,16,15,9,47,49,16,12,13,19,18,14,15,21,20,15,16,22,21,16,49,51,22,25,26,27,24,26,28,29,27,28,30,31,29,32,5,2,1,4,14,48,42,14,20,50,48,20,52,56,50,34,35,17,11,35,36,23,17,36,61,60,23,32,37,6,5,37,38,8,6,38,39,10,8,4,42,41,43,34,11,59,63,18,19,62,64,20,21,53,52,21,22,54,53,22,51,55,54,24,40,33,25 + } + } + LayerElementSmoothing: 0 { + Version: 102 + Name: "" + MappingInformationType: "ByPolygon" + ReferenceInformationType: "Direct" + Smoothing: *32 { + a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + } + } + Layer: 0 { + Version: 100 + LayerElement: { + Type: "LayerElementNormal" + TypedIndex: 0 + } + LayerElement: { + Type: "LayerElementSmoothing" + TypedIndex: 0 + } + LayerElement: { + Type: "LayerElementUV" + TypedIndex: 0 + } + } + } + Model: 1383396475840, "Model::Whitebox_Ground_1600x1600_A", "Null" { + Version: 232 + Properties70: { + P: "InheritType", "enum", "", "",1 + P: "ScalingMax", "Vector3D", "Vector", "",0,0,0 + P: "DefaultAttributeIndex", "int", "Integer", "",0 + P: "Lcl Translation", "Lcl Translation", "", "A",29.7404651641846,7148.201171875,0 + P: "MaxHandle", "int", "Integer", "UH",8 + } + Shading: T + Culling: "CullingOff" + } + Model: 1383396460496, "Model::Geom_Whitebox_Ground_1600x1600_A_LOD00", "Mesh" { + Version: 232 + Properties70: { + P: "InheritType", "enum", "", "",1 + P: "ScalingMax", "Vector3D", "Vector", "",0,0,0 + P: "DefaultAttributeIndex", "int", "Integer", "",0 + P: "Lcl Translation", "Lcl Translation", "", "A",-7.62939453125e-06,0,0 + P: "MaxHandle", "int", "Integer", "UH",7 + } + Shading: T + Culling: "CullingOff" + } +} + +; Object connections +;------------------------------------------------------------------ + +Connections: { + + ;Model::Whitebox_Ground_1600x1600_A, Model::RootNode + C: "OO",1383396475840,0 + + ;Model::Geom_Whitebox_Ground_1600x1600_A_LOD00, Model::Whitebox_Ground_1600x1600_A + C: "OO",1383396460496,1383396475840 + + ;NodeAttribute::, Model::Whitebox_Ground_1600x1600_A + C: "OO",1384966798384,1383396475840 + + ;Geometry::, Model::Geom_Whitebox_Ground_1600x1600_A_LOD00 + C: "OO",1385635998432,1383396460496 +} diff --git a/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.FBX.meta b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.FBX.meta new file mode 100644 index 0000000..2f28c41 --- /dev/null +++ b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.FBX.meta @@ -0,0 +1,96 @@ +fileFormatVersion: 2 +guid: b912c1b66b24d453183613aefe1b5e90 +ModelImporter: + serializedVersion: 19300 + internalIDToNameTable: [] + externalObjects: {} + materials: + materialImportMode: 1 + materialName: 0 + materialSearch: 1 + materialLocation: 1 + animations: + legacyGenerateAnimations: 4 + bakeSimulation: 0 + resampleCurves: 1 + optimizeGameObjects: 0 + motionNodeName: + rigImportErrors: + rigImportWarnings: + animationImportErrors: + animationImportWarnings: + animationRetargetingWarnings: + animationDoRetargetingWarnings: 0 + importAnimatedCustomProperties: 0 + importConstraints: 0 + animationCompression: 1 + animationRotationError: 0.5 + animationPositionError: 0.5 + animationScaleError: 0.5 + animationWrapMode: 0 + extraExposedTransformPaths: [] + extraUserProperties: [] + clipAnimations: [] + isReadable: 0 + meshes: + lODScreenPercentages: [] + globalScale: 1 + meshCompression: 0 + addColliders: 0 + useSRGBMaterialColor: 1 + sortHierarchyByName: 1 + importVisibility: 1 + importBlendShapes: 1 + importCameras: 1 + importLights: 1 + swapUVChannels: 0 + generateSecondaryUV: 0 + useFileUnits: 1 + keepQuads: 0 + weldVertices: 1 + preserveHierarchy: 0 + skinWeightsMode: 0 + maxBonesPerVertex: 4 + minBoneWeight: 0.001 + meshOptimizationFlags: -1 + indexFormat: 0 + secondaryUVAngleDistortion: 8 + secondaryUVAreaDistortion: 15.000001 + secondaryUVHardAngle: 88 + secondaryUVPackMargin: 4 + useFileScale: 1 + tangentSpace: + normalSmoothAngle: 60 + normalImportMode: 0 + tangentImportMode: 3 + normalCalculationMode: 4 + legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0 + blendShapeNormalImportMode: 1 + normalSmoothingSource: 0 + referencedClips: [] + importAnimation: 1 + humanDescription: + serializedVersion: 3 + human: [] + skeleton: [] + armTwist: 0.5 + foreArmTwist: 0.5 + upperLegTwist: 0.5 + legTwist: 0.5 + armStretch: 0.05 + legStretch: 0.05 + feetSpacing: 0 + globalScale: 1 + rootMotionBoneName: + hasTranslationDoF: 0 + hasExtraRoot: 0 + skeletonHasParents: 1 + lastHumanDescriptionAvatarSource: {instanceID: 0} + autoGenerateAvatarMappingIfUnspecified: 1 + animationType: 2 + humanoidOversampling: 1 + avatarSetup: 0 + additionalBone: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.prefab b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.prefab new file mode 100644 index 0000000..9fa9a77 --- /dev/null +++ b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.prefab @@ -0,0 +1,103 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &5123344914533893751 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 63904447334045665} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3} + m_Name: + m_EditorClassIdentifier: + DefaultGhostMode: 0 + SupportedGhostModes: 3 + OptimizationMode: 0 + Importance: 1 + prefabId: f29ad53f1fbf649919debe09947101b1 + Name: + UsePreSerialization: 0 + RefGameObjects: [] + RefGuids: + ComponentNames: [] + PrefabTypeOverrides: [] + SendTypeOverrides: [] + VariantOverrides: [] + SendForChild: [] +--- !u!1001 &8341482175533059816 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_RootOrder + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalPosition.x + value: -0.29740465 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalPosition.z + value: -71.48201 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalRotation.w + value: 0.7071068 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalRotation.x + value: -0.7071068 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -4216859302048453862, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -927199367670048503, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + propertyPath: m_Name + value: Whitebox_Ground_1600x1600_A + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: b912c1b66b24d453183613aefe1b5e90, type: 3} +--- !u!1 &63904447334045665 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: -927199367670048503, guid: b912c1b66b24d453183613aefe1b5e90, + type: 3} + m_PrefabInstance: {fileID: 8341482175533059816} + m_PrefabAsset: {fileID: 0} diff --git a/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.prefab.meta b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.prefab.meta new file mode 100644 index 0000000..5be3d40 --- /dev/null +++ b/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1049df3aa2892401e94cc5e6f2d6132e +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Prespawn/PreSpawnTests.cs b/Tests/Editor/Prespawn/PreSpawnTests.cs index 3e69812..f339950 100644 --- a/Tests/Editor/Prespawn/PreSpawnTests.cs +++ b/Tests/Editor/Prespawn/PreSpawnTests.cs @@ -397,7 +397,7 @@ public void MultipleSubscenes() } [Test] - [Ignore("DOTS-6619 - Currently failing frequently in CI due to a timeout waiting for a UTP message")] + [Ignore("DOTS-6619 Test instability, causes crash when loading subscenes")] public void ManyPrespawnedObjects() { const int SubSceneCount = 10; @@ -438,25 +438,42 @@ public void ManyPrespawnedObjects() var clientGhosts = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(typeof(GhostComponent), ComponentType.ReadOnly()) .ToComponentDataArray(Allocator.Temp); +#if !ENABLE_TRANSFORM_V1 + var clientGhostPos = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(typeof(GhostComponent), typeof(LocalTransform), ComponentType.ReadOnly()) + .ToComponentDataArray(Allocator.Temp); + var serverGhosts = testWorld.ServerWorld.EntityManager.CreateEntityQuery(typeof(GhostComponent), ComponentType.ReadOnly()) + .ToComponentDataArray(Allocator.Temp); + var serverGhostPos = testWorld.ServerWorld.EntityManager.CreateEntityQuery(typeof(GhostComponent), typeof(LocalTransform), ComponentType.ReadOnly()) + .ToComponentDataArray(Allocator.Temp); +#else var clientGhostPos = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(typeof(GhostComponent), typeof(Translation), ComponentType.ReadOnly()) .ToComponentDataArray(Allocator.Temp); var serverGhosts = testWorld.ServerWorld.EntityManager.CreateEntityQuery(typeof(GhostComponent), ComponentType.ReadOnly()) .ToComponentDataArray(Allocator.Temp); var serverGhostPos = testWorld.ServerWorld.EntityManager.CreateEntityQuery(typeof(GhostComponent), typeof(Translation), ComponentType.ReadOnly()) .ToComponentDataArray(Allocator.Temp); +#endif var serverPosLookup = new NativeParallelHashMap(serverGhostPos.Length, Allocator.Temp); Assert.AreEqual(clientGhostPos.Length, serverGhostPos.Length); // Fill a hashmap with mapping from server ghost id to server position for (int i = 0; i < serverGhosts.Length; ++i) { +#if !ENABLE_TRANSFORM_V1 + serverPosLookup.Add(serverGhosts[i].ghostId, serverGhostPos[i].Position); +#else serverPosLookup.Add(serverGhosts[i].ghostId, serverGhostPos[i].Value); +#endif } for (int i = 0; i < clientGhosts.Length; ++i) { Assert.IsTrue(PrespawnHelper.IsPrespawGhostId(clientGhosts[i].ghostId), "Prespawned ghosts not initialized"); // Verify that the client ghost id exists on the server with the same position Assert.IsTrue(serverPosLookup.TryGetValue(clientGhosts[i].ghostId, out var serverPos)); - Assert.AreEqual(clientGhostPos[i].Value, serverPos); +#if !ENABLE_TRANSFORM_V1 + Assert.LessOrEqual(math.distance(clientGhostPos[i].Position, serverPos), 0.001f); +#else + Assert.LessOrEqual(math.distance(clientGhostPos[i].Value, serverPos), 0.001f); +#endif // Remove the server ghost id which we already matched against to make sure htere are no duplicates serverPosLookup.Remove(clientGhosts[i].ghostId); } @@ -500,11 +517,10 @@ public void PrefabVariantAreHandledCorrectly() } [Test] - [Ignore("subscene header issue instabilities on CI")] public void PrefabModelsAreHandledCorrectly() { - var prefab = AssetDatabase.LoadAssetAtPath("Assets/Tests/PrespawnTests/Whitebox_Ground_1600x1600_A.prefab"); - var variant = AssetDatabase.LoadAssetAtPath("Assets/Tests/PrespawnTests/Whitebox_Ground_1600x1600_A Variant.prefab"); + var prefab = AssetDatabase.LoadAssetAtPath("Packages/com.unity.netcode/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A.prefab"); + var variant = AssetDatabase.LoadAssetAtPath("Packages/com.unity.netcode/Tests/Editor/Prespawn/Assets/Whitebox_Ground_1600x1600_A Variant.prefab"); var scene = SubSceneHelper.CreateEmptyScene(ScenePath, "Parent"); SubSceneHelper.CreateSubSceneWithPrefabs(scene,Path.GetDirectoryName(scene.path), "Sub0", new []{prefab, variant}, 2); SceneManager.SetActiveScene(scene); @@ -569,7 +585,7 @@ public void MulitpleSubScenesWithSameObjectsPositionAreHandledCorrectly() } } - [Test] + [Test, Ignore("Inconclusive CI error: Package Test - netcode [mac, trunk DOTS Monorepo]: [TimeoutExceptionMessage]: Timeout while waiting for a log message, no editor logging has happened during the timeout window! #6210")] public void MismatchedPrespawnClientServerScenesCantConnect() { var ghost = SubSceneHelper.CreateSimplePrefab(ScenePath, "ghost", typeof(GhostAuthoringComponent)); @@ -591,8 +607,13 @@ public void MismatchedPrespawnClientServerScenesCantConnect() var entities = query.ToEntityArray(Allocator.Temp); for (int i = 0; i < 10; ++i) { +#if !ENABLE_TRANSFORM_V1 + testWorld.ServerWorld.EntityManager.SetComponentData(entities[i], + LocalTransform.FromPosition(new float3(-10000, 10, 10 * i))); +#else testWorld.ServerWorld.EntityManager.SetComponentData(entities[i], new Translation{ Value = new float3(-10000, 10, 10 * i)}); +#endif } entities.Dispose(); diff --git a/Tests/Editor/RelevancyTests.cs b/Tests/Editor/RelevancyTests.cs index 20315bd..fa372fa 100644 --- a/Tests/Editor/RelevancyTests.cs +++ b/Tests/Editor/RelevancyTests.cs @@ -39,7 +39,7 @@ protected override void OnDestroy() protected override void OnUpdate() { - ref var ghostRelevancy = ref GetSingletonRW().ValueRW; + ref var ghostRelevancy = ref SystemAPI.GetSingletonRW().ValueRW; var relevancySet = ghostRelevancy.GhostRelevancySet; var clearDep = Job.WithCode(() => { relevancySet.Clear(); diff --git a/Tests/Editor/RpcTestSystems.cs b/Tests/Editor/RpcTestSystems.cs index 5fec2ab..390aa77 100644 --- a/Tests/Editor/RpcTestSystems.cs +++ b/Tests/Editor/RpcTestSystems.cs @@ -287,7 +287,7 @@ protected override void OnUpdate() { if (SendCount[worldId] > 0) { - var entity = GetSingletonEntity(); + var entity = SystemAPI.GetSingletonEntity(); var req = EntityManager.CreateEntity(); EntityManager.AddComponentData(req, Cmds[worldId]); EntityManager.AddComponentData(req, new SendRpcCommandRequestComponent {TargetConnection = Entity.Null}); @@ -379,7 +379,7 @@ public partial class FlawedClientRcpSendSystem : SystemBase protected override void OnUpdate() { - if (HasSingleton() && !HasSingleton() && SendCount > 0) + if (SystemAPI.HasSingleton() && !SystemAPI.HasSingleton() && SendCount > 0) { var req = EntityManager.CreateEntity(); EntityManager.AddComponentData(req, default(SimpleRpcCommand)); diff --git a/Tests/Editor/RpcTests.cs b/Tests/Editor/RpcTests.cs index d08dda4..e58b019 100644 --- a/Tests/Editor/RpcTests.cs +++ b/Tests/Editor/RpcTests.cs @@ -190,20 +190,7 @@ public void Rpc_MalformedPackets_ThrowsAndLogError() Assert.True(ServerMultipleRpcReceiveSystem.ReceivedCount[1] == SendCount); } } - - [Test] - [Ignore("changes in burst 1.3 made this test fail now. The FunctionPointers are are always initialized now")] - public void Rpc_ClientRegisterRpcCommandWithNullFunctionPtr_Throws() - { - - using (var testWorld = new NetCodeTestWorld()) - { - testWorld.Bootstrap(true, typeof(InvalidRpcCommandRequestSystem)); - Assert.Throws(()=>{testWorld.CreateWorlds(false, 1);}); - Assert.Throws(()=>{testWorld.CreateWorlds(true, 1);}); - } - } - + [Test] public void Rpc_CanSendMoreThanOnePacketPerFrame() { diff --git a/Tests/Editor/SnapshotDataBufferLookupTests.cs b/Tests/Editor/SnapshotDataBufferLookupTests.cs new file mode 100644 index 0000000..cc52136 --- /dev/null +++ b/Tests/Editor/SnapshotDataBufferLookupTests.cs @@ -0,0 +1,253 @@ +using NUnit.Framework; +using NUnit.Framework.Internal; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; + +namespace Unity.NetCode.Tests.Editor +{ + [DisableAutoCreation] + [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] + [UpdateInGroup(typeof(GhostSimulationSystemGroup))] + [UpdateAfter(typeof(GhostSpawnClassificationSystem))] + partial struct TestSpawnBufferClassifier : ISystem + { + private LowLevel.SnapshotDataLookupHelper lookupHelper; + private BufferLookup snapshotBufferLookup; + public int ClassifiedPredictedSpawns { get; private set; } + public void OnCreate(ref SystemState state) + { + lookupHelper = new LowLevel.SnapshotDataLookupHelper(ref state); + snapshotBufferLookup = state.GetBufferLookup(true); + state.RequireForUpdate(); + state.RequireForUpdate(); + state.RequireForUpdate(); + state.RequireForUpdate(); + } + + public void OnDestroy(ref SystemState state) + { + } + + public void OnUpdate(ref SystemState state) + { + lookupHelper.Update(ref state); + snapshotBufferLookup.Update(ref state); + var ghostMap = SystemAPI.GetSingleton().Value; + var ghostCollection = SystemAPI.GetSingletonEntity(); + var snapshotLookup = lookupHelper.CreateSnapshotBufferLookup(ghostCollection, ghostMap); + var predictedSpawnList = SystemAPI.GetSingletonBuffer(true); + + foreach (var (spawnBuffer, spawnDataBuffer) + in SystemAPI.Query, DynamicBuffer>() + .WithAll()) + { + for (int i = 0; i < spawnBuffer.Length; ++i) + { + UnityEngine.Debug.LogWarning($"Checking ghost {i}"); + var ghost = spawnBuffer[i]; + Assert.IsTrue(snapshotLookup.HasGhostOwner(ghost)); +#if !ENABLE_TRANSFORM_V1 + Assert.IsTrue(snapshotLookup.HasComponent(ghost.GhostType)); +#else + Assert.IsTrue(snapshotLookup.HasComponent(ghost.GhostType)); +#endif + Assert.IsTrue(snapshotLookup.HasComponent(ghost.GhostType)); + Assert.IsTrue(snapshotLookup.HasBuffer(ghost.GhostType)); + Assert.AreEqual(1, snapshotLookup.GetGhostOwner(ghost, spawnDataBuffer)); +#if !ENABLE_TRANSFORM_V1 + Assert.IsTrue(snapshotLookup.TryGetComponentDataFromSpawnBuffer(ghost, spawnDataBuffer, out LocalTransform transform)); + Assert.IsTrue(math.distance(new float3(40f, 10f, 90f), transform.Position) < 1.0e-4f); +#else + Assert.IsTrue(snapshotLookup.TryGetComponentDataFromSpawnBuffer(ghost, spawnDataBuffer, out Translation translation)); + Assert.IsTrue(math.distance(new float3(40f, 10f, 90f), translation.Value) < 1.0e-4f); +#endif + Assert.IsTrue(snapshotLookup.TryGetComponentDataFromSpawnBuffer(ghost, spawnDataBuffer, out SomeData someData)); + Assert.AreEqual(10000, someData.Value); + Assert.IsTrue(snapshotLookup.TryGetComponentDataFromSpawnBuffer(ghost, spawnDataBuffer, out GhostOwnerComponent ownerComponent)); + Assert.AreEqual(1, ownerComponent.NetworkId); + + if (ghost.SpawnType != GhostSpawnBuffer.Type.Predicted || ghost.HasClassifiedPredictedSpawn || ghost.PredictedSpawnEntity != Entity.Null) + continue; + for(int j=0;j(testWorld.ServerWorld).Length,1); + Assert.AreEqual(testWorld.GetSingletonBuffer(testWorld.ClientWorlds[0]).Length,1); + var serverGhost = testWorld.ServerWorld.EntityManager.Instantiate(testWorld.GetSingletonBuffer(testWorld.ServerWorld).ElementAt(0).GhostPrefab); + SetComponentsData(testWorld.ServerWorld, serverGhost); + for(var i=0;i<32;++i) + testWorld.Tick(1.0f/60f); + } + } + + [Test] + public void ComponentCanBeExtractedFromPredictedSpawnBuffer() + { + using (var testWorld = new NetCodeTestWorld()) + { + testWorld.Bootstrap(true, typeof(TestSpawnBufferClassifier)); + testWorld.CreateGhostCollection(); + testWorld.CreateWorlds(true, 1); + BuildPrefab(testWorld.ServerWorld.EntityManager, "TestPrefab"); + var clientPrefab = BuildPrefab(testWorld.ClientWorlds[0].EntityManager, "TestPrefab"); + var predictedSpawnVariant = CreatePredictedSpawnVariant(testWorld.ClientWorlds[0].EntityManager, clientPrefab); + Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(predictedSpawnVariant)); + testWorld.Connect(1f / 60f, 10); + testWorld.GoInGame(); + for(var i=0;i<32;++i) + testWorld.Tick(1.0f/60f); + Assert.AreEqual(testWorld.GetSingletonBuffer(testWorld.ServerWorld).Length,1); + Assert.AreEqual(testWorld.GetSingletonBuffer(testWorld.ClientWorlds[0]).Length,1); + //Predict the spawning on the client. And match the one coming from server + var clientGhost = testWorld.ClientWorlds[0].EntityManager.Instantiate(predictedSpawnVariant); + Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(clientGhost)); + SetComponentsData(testWorld.ClientWorlds[0], clientGhost); + for(var i=0;i<2;++i) + testWorld.Tick(1.0f/60f); + var serverGhost = testWorld.ServerWorld.EntityManager.Instantiate(testWorld.GetSingletonBuffer(testWorld.ServerWorld).ElementAt(0).GhostPrefab); + SetComponentsData(testWorld.ServerWorld, serverGhost); + for(var i=0;i<32;++i) + testWorld.Tick(1.0f/60f); + var classifier = testWorld.ClientWorlds[0].GetExistingSystem(); + var systemRef = testWorld.ClientWorlds[0].Unmanaged.GetUnsafeSystemRef(classifier); + Assert.AreEqual(1, systemRef.ClassifiedPredictedSpawns); + } + } + + [Test] + public void ComponentCanBeExtractedForDifferentGhostTypes() + { + using (var testWorld = new NetCodeTestWorld()) + { + testWorld.Bootstrap(true, typeof(TestSpawnBufferClassifier)); + testWorld.CreateGhostCollection(); + testWorld.CreateWorlds(true, 1); + var ghostsPrefabs = new Entity[5]; + for (int i = 0; i < ghostsPrefabs.Length; ++i) + { + BuildPrefab(testWorld.ServerWorld.EntityManager, $"TestPrefab_{i}"); + var clientPrefab = BuildPrefab(testWorld.ClientWorlds[0].EntityManager, $"TestPrefab_{i}"); + ghostsPrefabs[i] = CreatePredictedSpawnVariant(testWorld.ClientWorlds[0].EntityManager, clientPrefab); + Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(ghostsPrefabs[i])); + } + + + testWorld.Connect(1f / 60f, 10); + testWorld.GoInGame(); + for(var i=0;i<32;++i) + testWorld.Tick(1.0f/60f); + Assert.AreEqual(ghostsPrefabs.Length, testWorld.GetSingletonBuffer(testWorld.ServerWorld).Length); + Assert.AreEqual(ghostsPrefabs.Length, testWorld.GetSingletonBuffer(testWorld.ClientWorlds[0]).Length); + //Predict the spawning on the client. And match the one coming from server + for (int i = 0; i < ghostsPrefabs.Length; ++i) + { + var clientGhost = testWorld.ClientWorlds[0].EntityManager.Instantiate(ghostsPrefabs[i]); + Assert.IsTrue(testWorld.ClientWorlds[0].EntityManager.HasComponent(clientGhost)); + SetComponentsData(testWorld.ClientWorlds[0], clientGhost); + } + for(var i=0;i<2;++i) + testWorld.Tick(1.0f/60f); + + for (int i = 0; i < ghostsPrefabs.Length; ++i) + { + var serverGhost = testWorld.ServerWorld.EntityManager.Instantiate(testWorld.GetSingletonBuffer(testWorld.ServerWorld).ElementAt(i).GhostPrefab); + SetComponentsData(testWorld.ServerWorld, serverGhost); + } + for(var i=0;i<32;++i) + testWorld.Tick(1.0f/60f); + var classifier = testWorld.ClientWorlds[0].GetExistingSystem(); + var systemRef = testWorld.ClientWorlds[0].Unmanaged.GetUnsafeSystemRef(classifier); + Assert.AreEqual(ghostsPrefabs.Length, systemRef.ClassifiedPredictedSpawns); + } + } + + private void SetComponentsData(World world, Entity entity) + { +#if !ENABLE_TRANSFORM_V1 + world.EntityManager.SetComponentData(entity, LocalTransform.FromPosition(40f,10f, 90f)); +#else + world.EntityManager.SetComponentData(entity, new Translation{Value = new float3(40f,10f, 90f)}); +#endif + world.EntityManager.SetComponentData(entity, new GhostOwnerComponent { NetworkId = 1}); + world.EntityManager.SetComponentData(entity, new SomeData { Value = 10000 }); + world.EntityManager.GetBuffer(entity).Add(new GhostGenTest_Buffer{IntValue = 10}); + } + + private Entity BuildPrefab(EntityManager entityManager, string prefabName) + { + var archetype = entityManager.CreateArchetype( +#if !ENABLE_TRANSFORM_V1 + new ComponentType(typeof(Transforms.LocalTransform)), +#else + new ComponentType(typeof(Transforms.Translation)), +#endif + new ComponentType(typeof(GhostOwnerComponent)), + new ComponentType(typeof(GhostGenTest_Buffer)), + new ComponentType(typeof(SomeData))); + var prefab = entityManager.CreateEntity(archetype); + GhostPrefabCreation.ConvertToGhostPrefab(entityManager, prefab, new GhostPrefabCreation.Config + { + Name = prefabName, + Importance = 1000, + SupportedGhostModes = GhostModeMask.All, + DefaultGhostMode = GhostMode.OwnerPredicted, + OptimizationMode = GhostOptimizationMode.Dynamic, + UsePreSerialization = false + }); + return prefab; + } + + private Entity CreatePredictedSpawnVariant(EntityManager entityManager, Entity entity) + { + var predicted = entityManager.Instantiate(entity); + entityManager.AddComponent(entity); + if (entityManager.HasComponent(predicted)) + { + var leg = entityManager.GetBuffer(predicted, true); + foreach (var ent in leg) + entityManager.AddComponent(ent.Value); + } + entityManager.AddComponent(predicted); + return predicted; + } + } +} diff --git a/Tests/Editor/SnapshotDataBufferLookupTests.cs.meta b/Tests/Editor/SnapshotDataBufferLookupTests.cs.meta new file mode 100644 index 0000000..cd3c9ca --- /dev/null +++ b/Tests/Editor/SnapshotDataBufferLookupTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 028ed18aafd647a5a5dd7a29c9bd0dc5 +timeCreated: 1657909632 \ No newline at end of file diff --git a/Tests/Editor/SpawnTests.cs b/Tests/Editor/SpawnTests.cs index ccfcb43..9fced82 100644 --- a/Tests/Editor/SpawnTests.cs +++ b/Tests/Editor/SpawnTests.cs @@ -73,7 +73,7 @@ protected override void OnDestroy() protected override void OnUpdate() { - var spawnListEntity = GetSingletonEntity(); + var spawnListEntity = SystemAPI.GetSingletonEntity(); var spawnListFromEntity = GetBufferLookup(); var predictedEntities = PredictedEntities; Entities diff --git a/Tests/Editor/StaticOptimizationTests.cs b/Tests/Editor/StaticOptimizationTests.cs index f283cb8..965fc55 100644 --- a/Tests/Editor/StaticOptimizationTests.cs +++ b/Tests/Editor/StaticOptimizationTests.cs @@ -29,11 +29,19 @@ public partial class StaticOptimizationTestSystem : SystemBase protected override void OnUpdate() { int modifyNetworkId = s_ModifyNetworkId; +#if !ENABLE_TRANSFORM_V1 + Entities.ForEach((ref LocalTransform trans, in GhostOwnerComponent ghostOwner) => { + if (ghostOwner.NetworkId != modifyNetworkId) + return; + trans.Position.x += 1; + }).ScheduleParallel(); +#else Entities.ForEach((ref Translation trans, in GhostOwnerComponent ghostOwner) => { if (ghostOwner.NetworkId != modifyNetworkId) return; trans.Value.x += 1; }).ScheduleParallel(); +#endif } } diff --git a/Tests/Editor/SubSceneLoadingTests.cs b/Tests/Editor/SubSceneLoadingTests.cs index c930344..12fecd5 100644 --- a/Tests/Editor/SubSceneLoadingTests.cs +++ b/Tests/Editor/SubSceneLoadingTests.cs @@ -25,7 +25,7 @@ public partial class LoadingGhostCollectionSystem : SystemBase { protected override void OnUpdate() { - var collectionEntity = GetSingletonEntity(); + var collectionEntity = SystemAPI.GetSingletonEntity(); var ghostCollection = EntityManager.GetBuffer(collectionEntity); var subScenes = GetEntityQuery(ComponentType.ReadOnly()).ToEntityArray(Allocator.Temp); var anyLoaded = false; @@ -58,10 +58,17 @@ protected override void OnUpdate() float deltaTime = SystemAPI.Time.DeltaTime; Entities .WithAll() +#if !ENABLE_TRANSFORM_V1 + .ForEach((ref LocalTransform transform) => + { + transform.Position = new float3(transform.Position.x, transform.Position.y + deltaTime*60.0f, transform.Position.z); + }).Schedule(); +#else .ForEach((ref Translation translation) => { translation.Value = new float3(translation.Value.x, translation.Value.y + deltaTime*60.0f, translation.Value.z); }).Schedule(); +#endif } } @@ -168,7 +175,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE // This job is not written to support queries with enableable component types. Assert.IsFalse(useEnabledMask); - var array = chunk.GetNativeArray(someDataHandle); + var array = chunk.GetNativeArray(ref someDataHandle); for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i) { array[i] = new SomeData {Value = offset + i}; @@ -420,8 +427,7 @@ public void ServerInitiatedSceneUnload() //Server will unload the first scene. This will despawn ghosts and also update the scene list SceneSystem.UnloadScene(testWorld.ServerWorld.Unmanaged, sub0.SceneGUID, SceneSystem.UnloadParameters.DestroySceneProxyEntity| - SceneSystem.UnloadParameters.DestroySectionProxyEntities| - SceneSystem.UnloadParameters.DestroySubSceneProxyEntities); + SceneSystem.UnloadParameters.DestroySectionProxyEntities); for (int i = 0; i < 16; ++i) { testWorld.Tick(frameTime); @@ -440,8 +446,7 @@ public void ServerInitiatedSceneUnload() SceneSystem.UnloadScene(testWorld.ClientWorlds[0].Unmanaged, sub0.SceneGUID, SceneSystem.UnloadParameters.DestroySceneProxyEntity | - SceneSystem.UnloadParameters.DestroySectionProxyEntities | - SceneSystem.UnloadParameters.DestroySubSceneProxyEntities); + SceneSystem.UnloadParameters.DestroySectionProxyEntities); //And nothing should break for (int i = 0; i < 16; ++i) { @@ -512,16 +517,24 @@ public void ClientLoadUnloadScene() var subSceneEntity = testWorld.TryGetSingletonEntity(testWorld.ClientWorlds[0]); Assert.AreNotEqual(Entity.Null, subSceneEntity); //Only 5 ghost should be present +#if !ENABLE_TRANSFORM_V1 + var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(ComponentType.ReadOnly(), ComponentType.ReadOnly()); +#else var query = testWorld.ClientWorlds[0].EntityManager.CreateEntityQuery(ComponentType.ReadOnly(), ComponentType.ReadOnly()); +#endif Assert.AreEqual(numObjects, query.CalculateEntityCount()); //Now I should receive the ghost with their state changed for (int i = 0; i < 16; ++i) testWorld.Tick(frameTime); +#if !ENABLE_TRANSFORM_V1 + using var translations = query.ToComponentDataArray(Allocator.TempJob); +#else using var translations = query.ToComponentDataArray(Allocator.TempJob); +#endif for (int i = 0; i < translations.Length; ++i) - Assert.AreNotEqual(0.0f, translations[i].Value); + Assert.AreNotEqual(0.0f, translations[i]); //Unload the scene on the client SceneSystem.UnloadScene( diff --git a/Tests/Editor/SubSceneLoadingTests_CustomAckFlow.cs b/Tests/Editor/SubSceneLoadingTests_CustomAckFlow.cs index cbb6f2e..f8e448d 100644 --- a/Tests/Editor/SubSceneLoadingTests_CustomAckFlow.cs +++ b/Tests/Editor/SubSceneLoadingTests_CustomAckFlow.cs @@ -39,10 +39,10 @@ protected override void OnCreate() protected override void OnUpdate() { var ecb = m_Barrier.CreateCommandBuffer(); - var serverTick = GetSingleton().ServerTick; + var serverTick = SystemAPI.GetSingleton().ServerTick; Entities.ForEach((Entity entity, in NotifySceneLoaded streamingReq, in ReceiveRpcCommandRequestComponent requestComponent) => { - var prespawnSceneAcks = GetBuffer(requestComponent.SourceConnection); + var prespawnSceneAcks = SystemAPI.GetBuffer(requestComponent.SourceConnection); int ackIdx = prespawnSceneAcks.IndexOf(streamingReq.SceneHash); if (ackIdx == -1) prespawnSceneAcks.Add(new PrespawnSectionAck { SceneHash = streamingReq.SceneHash }); @@ -51,7 +51,7 @@ protected override void OnUpdate() Entities.ForEach((Entity entity, in NotifyUnloadingScene streamingReq, in ReceiveRpcCommandRequestComponent requestComponent) => { - var prespawnSceneAcks = GetBuffer(requestComponent.SourceConnection); + var prespawnSceneAcks = SystemAPI.GetBuffer(requestComponent.SourceConnection); int ackIdx = prespawnSceneAcks.IndexOf(streamingReq.SceneHash); if (ackIdx != -1) { diff --git a/Tests/Editor/TestEnterExitGame.cs b/Tests/Editor/TestEnterExitGame.cs index a82eb96..2ad1747 100644 --- a/Tests/Editor/TestEnterExitGame.cs +++ b/Tests/Editor/TestEnterExitGame.cs @@ -19,8 +19,7 @@ private void UnloadSubScene(World world) SceneSystem.UnloadScene(world.Unmanaged, subScene.SceneGUID, SceneSystem.UnloadParameters.DestroySceneProxyEntity| - SceneSystem.UnloadParameters.DestroySectionProxyEntities| - SceneSystem.UnloadParameters.DestroySubSceneProxyEntities); + SceneSystem.UnloadParameters.DestroySectionProxyEntities); } [Test] diff --git a/Tests/Utils/NetCodeScenarioUtils.cs b/Tests/Utils/NetCodeScenarioUtils.cs index f9d6100..7e925bd 100644 --- a/Tests/Utils/NetCodeScenarioUtils.cs +++ b/Tests/Utils/NetCodeScenarioUtils.cs @@ -173,7 +173,7 @@ protected override void OnStartRunning() public void ConfigureSendSystem(NetcodeScenarioUtils.ScenarioParams parameters) { - ref var ghostSendSystemData = ref GetSingletonRW().ValueRW; + ref var ghostSendSystemData = ref SystemAPI.GetSingletonRW().ValueRW; ghostSendSystemData.ForceSingleBaseline = parameters.GhostSystemParams.ForceSingleBaseline; ghostSendSystemData.ForcePreSerialize = parameters.GhostSystemParams.ForcePreSerialize; } @@ -202,7 +202,7 @@ public void SetupStats(int prefabCount, NetcodeScenarioUtils.ScenarioParams para protected override void OnUpdate() { - var numLoadedPrefabs= GetSingleton().NumLoadedPrefabs; + var numLoadedPrefabs= SystemAPI.GetSingleton().NumLoadedPrefabs; var markers = new string[] { @@ -229,7 +229,7 @@ protected override void OnUpdate() EntityManager.CompleteAllTrackedJobs(); #if UNITY_EDITOR || DEVELOPMENT_BUILD - var netStats = GetSingletonRW().ValueRW; + var netStats = SystemAPI.GetSingletonRW().ValueRW; for (int worker = 1; worker < netStats.Workers; ++worker) { int statOffset = worker * netStats.Stride; diff --git a/Tests/Utils/NetCodeTestWorld.cs b/Tests/Utils/NetCodeTestWorld.cs index e14676e..d392634 100644 --- a/Tests/Utils/NetCodeTestWorld.cs +++ b/Tests/Utils/NetCodeTestWorld.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using NUnit.Framework; using Unity.Core; using Unity.Entities; -using Unity.NetCode; using Unity.Networking.Transport; using Unity.Networking.Transport.Utilities; using Unity.Collections; @@ -15,7 +12,6 @@ using Unity.Transforms; using Debug = UnityEngine.Debug; #if UNITY_EDITOR -using Unity.NetCode.Editor; using UnityEngine; #endif @@ -177,7 +173,7 @@ private static bool IsFromNetCodeAssembly(Type sys) sys.Assembly.FullName.StartsWith("Unity.NetCode.EditorTests,") || sys.Assembly.FullName.StartsWith("Unity.NetCode.TestsUtils,") || sys.Assembly.FullName.StartsWith("Unity.NetCode.Physics.EditorTests,") || - typeof(GhostComponentSerializerRegistrationSystemBase).IsAssignableFrom(sys); + typeof(IGhostComponentSerializerRegistration).IsAssignableFrom(sys); } public void Bootstrap(bool includeNetCodeSystems, params Type[] userSystems) @@ -194,11 +190,6 @@ public void Bootstrap(bool includeNetCodeSystems, params Type[] userSystems) m_ControlSystems.Add(typeof(TickServerSimulationSystem)); m_ControlSystems.Add(typeof(DriverMigrationSystem)); - UserBakingSystems.Add(typeof(TestWorldDefaultVariantSystem)); - m_ClientSystems.Add(typeof(TestWorldDefaultVariantSystem)); - m_ThinClientSystems.Add(typeof(TestWorldDefaultVariantSystem)); - m_ServerSystems.Add(typeof(TestWorldDefaultVariantSystem)); - if (s_NetCodeClientSystems == null) { s_NetCodeClientSystems = new List(); @@ -784,18 +775,28 @@ private Entity BakeGameObject(GameObject go, World world, BlobAssetStore blobAss using var intermediateWorld = new World("NetCodeBakingWorld"); var bakingSettings = new BakingSettings(BakingUtility.BakingFlags.AddEntityGUID, blobAssetStore); + bakingSettings.PrefabRoot = go; bakingSettings.ExtraSystems.AddRange(UserBakingSystems); - BakingUtility.BakeGameObjects(intermediateWorld, new GameObject[] {go}, bakingSettings); + BakingUtility.BakeGameObjects(intermediateWorld, new GameObject[] {}, bakingSettings); var bakingSystem = intermediateWorld.GetExistingSystemManaged(); var intermediateEntity = bakingSystem.GetEntity(go); var intermediateEntityGuid = intermediateWorld.EntityManager.GetComponentData(intermediateEntity); + // Copy all the tracked/baked entities. That TransformAuthoring is present on all entities added by the baker for the + // converted gameobject. It is sufficient condition to copy all the additional entities as well. +#if !ENABLE_TRANSFORM_V1 + var builder = new EntityQueryBuilder(Allocator.Temp) + .WithAll(); +#else + var builder = new EntityQueryBuilder(Allocator.Temp) + .WithAll(); +#endif - // Copy the world - world.EntityManager.MoveEntitiesFrom(intermediateWorld.EntityManager); + using var bakedEntities = intermediateWorld.EntityManager.CreateEntityQuery(builder); + world.EntityManager.MoveEntitiesFrom(intermediateWorld.EntityManager, bakedEntities); // Search for the entity in the final world by comparing the EntityGuid from entity in the intermediate world - var query = world.EntityManager.CreateEntityQuery(new ComponentType[] {typeof(EntityGuid)}); + using var query = world.EntityManager.CreateEntityQuery(typeof(EntityGuid), typeof(Prefab)); using var entityArray = query.ToEntityArray(Allocator.TempJob); using var entityGUIDs = query.ToComponentDataArray(Allocator.TempJob); for (int index = 0; index < entityGUIDs.Length; ++index) @@ -845,15 +846,4 @@ public Entity BakeGhostCollection(World world) } #endif } - - /// Register variants for test world. - [DisableAutoCreation] - public sealed class TestWorldDefaultVariantSystem : DefaultVariantSystemBase - { - protected override void RegisterDefaultVariants(Dictionary defaultVariants) - { - defaultVariants.Add(typeof(Rotation), Rule.OnlyParents(typeof(RotationDefaultVariant))); - defaultVariants.Add(typeof(Translation), Rule.OnlyParents(typeof(TranslationDefaultVariant))); - } - } } diff --git a/Tests/Utils/TestWithSceneAsset.cs b/Tests/Utils/TestWithSceneAsset.cs index fefc126..5309bac 100644 --- a/Tests/Utils/TestWithSceneAsset.cs +++ b/Tests/Utils/TestWithSceneAsset.cs @@ -1,7 +1,6 @@ #if UNITY_EDITOR using System; using System.IO; -using Authoring.Hybrid; using NUnit.Framework; using Unity.Entities.Build; using UnityEditor; @@ -18,8 +17,6 @@ public abstract class TestWithSceneAsset [SetUp] public void SetupScene() { - DotsPlayerSettings.AdditionalBakingSystemsTemp.Add(typeof(TestWorldDefaultVariantSystem)); - ScenePath = Path.Combine("Assets", Path.GetRandomFileName()); Directory.CreateDirectory(ScenePath); LastWriteTime = Directory.GetLastWriteTime(Application.dataPath + ScenePath); @@ -28,8 +25,6 @@ public void SetupScene() [TearDown] public void DestroyScenes() { - DotsPlayerSettings.AdditionalBakingSystemsTemp.Remove(typeof(TestWorldDefaultVariantSystem)); - foreach (var go in SceneManager.GetActiveScene().GetRootGameObjects()) UnityEngine.Object.DestroyImmediate(go); diff --git a/ValidationExceptions.json b/ValidationExceptions.json index 569e6af..d082d1c 100644 --- a/ValidationExceptions.json +++ b/ValidationExceptions.json @@ -3,7 +3,7 @@ { "ValidationTest": "API Validation", "ExceptionMessage": "For Experimental or Preview Packages, breaking changes require a new minor version.", - "PackageVersion": "1.0.0-exp.3" + "PackageVersion": "0.60.0-preview.61" } ], "WarningExceptions": [] diff --git a/package.json b/package.json index 4a61f15..80d0e24 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,22 @@ { "name": "com.unity.netcode", "displayName": "Netcode for Entities", - "version": "1.0.0-exp.13", + "version": "1.0.0-pre.15", "unity": "2022.2", - "unityRelease": "0b5", + "unityRelease": "0f1", "description": "Unity's Data Oriented Technology Stack (DOTS) multiplayer netcode layer - a high level netcode system built on entities. This package provides a foundation for creating networked multiplayer applications within DOTS.", "dependencies": { - "com.unity.transport": "2.0.0-exp.6", - "com.unity.entities": "1.0.0-exp.12", - "com.unity.logging": "1.0.0-exp.7" + "com.unity.transport": "2.0.0-pre.2", + "com.unity.entities": "1.0.0-pre.15", + "com.unity.logging": "1.0.0-pre.11", + "com.unity.modules.animation": "1.0.0" }, "upmCi": { - "footprint": "13592f569f5c2ba8c63aa6a77a3e13fd9942f6bf" + "footprint": "6949a45917c3cdc0f14c0a56025b2a032c793bb5" }, "repository": { "url": "https://github.cds.internal.unity3d.com/unity/dots.git", "type": "git", - "revision": "bde89ab29c5e7b9e2491079103f23ad23d8dc81f" + "revision": "82387d7cb4bc4aee07e94656ef8103860ef6ec55" } }