com.unity.netcode.gameobjects@1.0.0-pre.2

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).

## [1.0.0-pre.2] - 2020-12-20

### Added

- Associated Known Issues for the 1.0.0-pre.1 release in the changelog

### Changed

- Updated label for `1.0.0-pre.1` changelog section

## [1.0.0-pre.1] - 2020-12-20

### Added

- Added `ClientNetworkTransform` sample to the SDK package (#1168)
- Added `Bootstrap` sample to the SDK package (#1140)
- Enhanced `NetworkSceneManager` implementation with additive scene loading capabilities (#1080, #955, #913)
  - `NetworkSceneManager.OnSceneEvent` provides improved scene event notificaitons
- Enhanced `NetworkTransform` implementation with per axis/component based and threshold based state replication (#1042, #1055, #1061, #1084, #1101)
- Added a jitter-resistent `BufferedLinearInterpolator<T>` for `NetworkTransform` (#1060)
- Implemented `NetworkPrefabHandler` that provides support for object pooling and `NetworkPrefab` overrides (#1073, #1004, #977, #905,#749, #727)
- Implemented auto `NetworkObject` transform parent synchronization at runtime over the network (#855)
- Adopted Unity C# Coding Standards in the codebase with `.editorconfig` ruleset (#666, #670)
- When a client tries to spawn a `NetworkObject` an exception is thrown to indicate unsupported behavior. (#981)
- Added a `NetworkTime` and `NetworkTickSystem` which allows for improved control over time and ticks. (#845)
- Added a `OnNetworkDespawn` function to `NetworkObject` which gets called when a `NetworkObject` gets despawned and can be overriden. (#865)
- Added `SnapshotSystem` that would allow variables and spawn/despawn messages to be sent in blocks (#805, #852, #862, #963, #1012, #1013, #1021, #1040, #1062, #1064, #1083, #1091, #1111, #1129, #1166, #1192)
  - Disabled by default for now, except spawn/despawn messages
  - Will leverage unreliable messages with eventual consistency
- `NetworkBehaviour` and `NetworkObject`'s `NetworkManager` instances can now be overriden (#762)
- Added metrics reporting for the new network profiler if the Multiplayer Tools package is present (#1104, #1089, #1096, #1086, #1072, #1058, #960, #897, #891, #878)
- `NetworkBehaviour.IsSpawned` a quick (and stable) way to determine if the associated NetworkObject is spawned (#1190)
- Added `NetworkRigidbody` and `NetworkRigidbody2D` components to support networking `Rigidbody` and `Rigidbody2D` components (#1202, #1175)
- Added `NetworkObjectReference` and `NetworkBehaviourReference` structs which allow to sending `NetworkObject/Behaviours` over RPCs/`NetworkVariable`s (#1173)
- Added `NetworkAnimator` component to support networking `Animator` component (#1281, #872)

### Changed

- Bumped minimum Unity version, renamed package as "Unity Netcode for GameObjects", replaced `MLAPI` namespace and its variants with `Unity.Netcode` namespace and per asm-def variants (#1007, #1009, #1015, #1017, #1019, #1025, #1026, #1065)
  - Minimum Unity version:
    - 2019.4 → 2020.3+
  - Package rename:
    - Display name: `MLAPI Networking Library` → `Netcode for GameObjects`
    - Name: `com.unity.multiplayer.mlapi` → `com.unity.netcode.gameobjects`
    - Updated package description
  - All `MLAPI.x` namespaces are replaced with `Unity.Netcode`
    - `MLAPI.Messaging` → `Unity.Netcode`
    - `MLAPI.Connection` → `Unity.Netcode`
    - `MLAPI.Logging` → `Unity.Netcode`
    - `MLAPI.SceneManagement` → `Unity.Netcode`
    - and other `MLAPI.x` variants to `Unity.Netcode`
  - All assembly definitions are renamed with `Unity.Netcode.x` variants
    - `Unity.Multiplayer.MLAPI.Runtime` → `Unity.Netcode.Runtime`
    - `Unity.Multiplayer.MLAPI.Editor` → `Unity.Netcode.Editor`
    - and other `Unity.Multiplayer.MLAPI.x` variants to `Unity.Netcode.x` variants
- Renamed `Prototyping` namespace and assembly definition to `Components` (#1145)
- Changed `NetworkObject.Despawn(bool destroy)` API to default to `destroy = true` for better usability (#1217)
- Scene registration in `NetworkManager` is now replaced by Build Setttings → Scenes in Build List (#1080)
- `NetworkSceneManager.SwitchScene` has been replaced by `NetworkSceneManager.LoadScene` (#955)
- `NetworkManager, NetworkConfig, and NetworkSceneManager` scene registration replaced with scenes in build list (#1080)
- `GlobalObjectIdHash` replaced `PrefabHash` and `PrefabHashGenerator` for stability and consistency (#698)
- `NetworkStart` has been renamed to `OnNetworkSpawn`. (#865)
- Network variable cleanup - eliminated shared mode, variables are server-authoritative (#1059, #1074)
- `NetworkManager` and other systems are no longer singletons/statics (#696, #705, #706, #737, #738, #739, #746, #747, #763, #765, #766, #783, #784, #785, #786, #787, #788)
- Changed `INetworkSerializable.NetworkSerialize` method signature to use `BufferSerializer<T>` instead of `NetworkSerializer` (#1187)
- Changed `CustomMessagingManager`'s methods to use `FastBufferWriter` and `FastBufferReader` instead of `Stream` (#1187)
- Reduced internal runtime allocations by removing LINQ calls and replacing managed lists/arrays with native collections (#1196)

### Removed

- Removed `NetworkNavMeshAgent` (#1150)
- Removed `NetworkDictionary`, `NetworkSet` (#1149)
- Removed `NetworkVariableSettings` (#1097)
- Removed predefined `NetworkVariable<T>` types (#1093)
    - Removed `NetworkVariableBool`, `NetworkVariableByte`, `NetworkVariableSByte`, `NetworkVariableUShort`, `NetworkVariableShort`, `NetworkVariableUInt`, `NetworkVariableInt`, `NetworkVariableULong`, `NetworkVariableLong`, `NetworkVariableFloat`, `NetworkVariableDouble`, `NetworkVariableVector2`, `NetworkVariableVector3`, `NetworkVariableVector4`, `NetworkVariableColor`, `NetworkVariableColor32`, `NetworkVariableRay`, `NetworkVariableQuaternion`
- Removed `NetworkChannel` and `MultiplexTransportAdapter` (#1133)
- Removed ILPP backend for 2019.4, minimum required version is 2020.3+ (#895)
- `NetworkManager.NetworkConfig` had the following properties removed: (#1080)
  - Scene Registrations no longer exists
  - Allow Runtime Scene Changes was no longer needed and was removed
- Removed the NetworkObject.Spawn payload parameter (#1005)
- Removed `ProfilerCounter`, the original MLAPI network profiler, and the built-in network profiler module (2020.3). A replacement can now be found in the Multiplayer Tools package. (#1048)
- Removed UNet RelayTransport and related relay functionality in UNetTransport (#1081)
- Removed `UpdateStage` parameter from `ServerRpcSendParams` and `ClientRpcSendParams` (#1187)
- Removed `NetworkBuffer`, `NetworkWriter`, `NetworkReader`, `NetworkSerializer`, `PooledNetworkBuffer`, `PooledNetworkWriter`, and `PooledNetworkReader` (#1187)
- Removed `EnableNetworkVariable` in `NetworkConfig`, it is always enabled now (#1179)
- Removed `NetworkTransform`'s FixedSendsPerSecond, AssumeSyncedSends, InterpolateServer, ExtrapolatePosition, MaxSendsToExtrapolate, Channel, EnableNonProvokedResendChecks, DistanceSendrate (#1060) (#826) (#1042, #1055, #1061, #1084, #1101)
- Removed `NetworkManager`'s `StopServer()`, `StopClient()` and `StopHost()` methods and replaced with single `NetworkManager.Shutdown()` method for all (#1108)

### Fixed

- Fixed ServerRpc ownership check to `Debug.LogError` instead of `Debug.LogWarning` (#1126)
- Fixed `NetworkObject.OwnerClientId` property changing before `NetworkBehaviour.OnGainedOwnership()` callback (#1092)
- Fixed `NetworkBehaviourILPP` to iterate over all types in an assembly (#803)
- Fixed cross-asmdef RPC ILPP by importing types into external assemblies (#678)
- Fixed `NetworkManager` shutdown when quitting the application or switching scenes (#1011)
  - Now `NetworkManager` shutdowns correctly and despawns existing `NetworkObject`s
- Fixed Only one `PlayerPrefab` can be selected on `NetworkManager` inspector UI in the editor (#676)
- Fixed connection approval not being triggered for host (#675)
- Fixed various situations where messages could be processed in an invalid order, resulting in errors (#948, #1187, #1218)
- Fixed `NetworkVariable`s being default-initialized on the client instead of being initialized with the desired value (#1266)
- Improved runtime performance and reduced GC pressure (#1187)
- Fixed #915 - clients are receiving data from objects not visible to them (#1099)
- Fixed `NetworkTransform`'s "late join" issues, `NetworkTransform` now uses `NetworkVariable`s instead of RPCs (#826)
- Throw an exception for silent failure when a client tries to get another player's `PlayerObject`, it is now only allowed on the server-side (#844)

### Known Issues

- `NetworkVariable` does not serialize `INetworkSerializable` types through their `NetworkSerialize` implementation
- `NetworkObjects` marked as `DontDestroyOnLoad` are disabled during some network scene transitions
- `NetworkTransform` interpolates from the origin when switching Local Space synchronization
- Exceptions thrown in `OnNetworkSpawn` user code for an object will prevent the callback in other objects
- Cannot send an array of `INetworkSerializable` in RPCs
- ILPP generation fails with special characters in project path

## [0.2.0] - 2021-06-03

WIP version increment to pass package validation checks. Changelog & final version number TBD.

## [0.1.1] - 2021-06-01

This is hotfix v0.1.1 for the initial experimental Unity MLAPI Package.

### Changed

- Fixed issue with the Unity Registry package version missing some fixes from the v0.1.0 release.

## [0.1.0] - 2021-03-23

This is the initial experimental Unity MLAPI Package, v0.1.0.

### Added

- Refactored a new standard for Remote Procedure Call (RPC) in MLAPI which provides increased performance, significantly reduced boilerplate code, and extensibility for future-proofed code. MLAPI RPC includes `ServerRpc` and `ClientRpc` to execute logic on the server and client-side. This provides a single performant unified RPC solution, replacing MLAPI Convenience and Performance RPC (see [here](#removed-features)).
- Added standarized serialization types, including built-in and custom serialization flows. See [RFC #2](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0002-serializable-types.md) for details.
- `INetworkSerializable` interface replaces `IBitWritable`.
- Added `NetworkSerializer`..., which is the main aggregator that implements serialization code for built-in supported types and holds `NetworkReader` and `NetworkWriter` instances internally.
- Added a Network Update Loop infrastructure that aids Netcode systems to update (such as RPC queue and transport) outside of the standard `MonoBehaviour` event cycle. See [RFC #8](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0008-network-update-loop.md) and the following details:
  - It uses Unity's [low-level Player Loop API](https://docs.unity3d.com/ScriptReference/LowLevel.PlayerLoop.html) and allows for registering `INetworkUpdateSystem`s with `NetworkUpdate` methods to be executed at specific `NetworkUpdateStage`s, which may also be before or after `MonoBehaviour`-driven game logic execution.
  - You will typically interact with `NetworkUpdateLoop` for registration and `INetworkUpdateSystem` for implementation.
  - `NetworkVariable`s are now tick-based using the `NetworkTickSystem`, tracking time through network interactions and syncs.
- Added message batching to handle consecutive RPC requests sent to the same client. `RpcBatcher` sends batches based on requests from the `RpcQueueProcessing`, by batch size threshold or immediately.
- [GitHub 494](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/494): Added a constraint to allow one `NetworkObject` per `GameObject`, set through the `DisallowMultipleComponent` attribute.
- Integrated MLAPI with the Unity Profiler for versions 2020.2 and later:
  - Added new profiler modules for MLAPI that report important network data.
  - Attached the profiler to a remote player to view network data over the wire.
- A test project is available for building and experimenting with MLAPI features. This project is available in the MLAPI GitHub [testproject folder](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/tree/release/0.1.0/testproject).
- Added a [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) new GitHub repository to accept extensions from the MLAPI community. Current extensions include moved MLAPI features for lag compensation (useful for Server Authoritative actions) and `TrackedObject`.

### Changed

- [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): MLAPI now uses the Unity Package Manager for installation management.
- Added functionality and usability to `NetworkVariable`, previously called `NetworkVar`. Updates enhance options and fully replace the need for `SyncedVar`s.
- [GitHub 507](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/507): Reimplemented `NetworkAnimator`, which synchronizes animation states for networked objects.
- GitHub [444](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/444) and [455](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/455): Channels are now represented as bytes instead of strings.

For users of previous versions of MLAPI, this release renames APIs due to refactoring. All obsolete marked APIs have been removed as per [GitHub 513](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/513) and [GitHub 514](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/514).

| Previous MLAPI Versions | V 0.1.0 Name |
| -- | -- |
| `NetworkingManager` | `NetworkManager` |
| `NetworkedObject` | `NetworkObject` |
| `NetworkedBehaviour` | `NetworkBehaviour` |
| `NetworkedClient` | `NetworkClient` |
| `NetworkedPrefab` | `NetworkPrefab` |
| `NetworkedVar` | `NetworkVariable` |
| `NetworkedTransform` | `NetworkTransform` |
| `NetworkedAnimator` | `NetworkAnimator` |
| `NetworkedAnimatorEditor` | `NetworkAnimatorEditor` |
| `NetworkedNavMeshAgent` | `NetworkNavMeshAgent` |
| `SpawnManager` | `NetworkSpawnManager` |
| `BitStream` | `NetworkBuffer` |
| `BitReader` | `NetworkReader` |
| `BitWriter` | `NetworkWriter` |
| `NetEventType` | `NetworkEventType` |
| `ChannelType` | `NetworkDelivery` |
| `Channel` | `NetworkChannel` |
| `Transport` | `NetworkTransport` |
| `NetworkedDictionary` | `NetworkDictionary` |
| `NetworkedList` | `NetworkList` |
| `NetworkedSet` | `NetworkSet` |
| `MLAPIConstants` | `NetworkConstants` |
| `UnetTransport` | `UNetTransport` |

### Fixed

- [GitHub 460](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/460): Fixed an issue for RPC where the host-server was not receiving RPCs from the host-client and vice versa without the loopback flag set in `NetworkingManager`.
- Fixed an issue where data in the Profiler was incorrectly aggregated and drawn, which caused the profiler data to increment indefinitely instead of resetting each frame.
- Fixed an issue the client soft-synced causing PlayMode client-only scene transition issues, caused when running the client in the editor and the host as a release build. Users may have encountered a soft sync of `NetworkedInstanceId` issues in the `SpawnManager.ClientCollectSoftSyncSceneObjectSweep` method.
- [GitHub 458](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/458): Fixed serialization issues in `NetworkList` and `NetworkDictionary` when running in Server mode.
- [GitHub 498](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/498): Fixed numerical precision issues to prevent not a number (NaN) quaternions.
- [GitHub 438](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/438): Fixed booleans by reaching or writing bytes instead of bits.
- [GitHub 519](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/519): Fixed an issue where calling `Shutdown()` before making `NetworkManager.Singleton = null` is null on `NetworkManager.OnDestroy()`.

### Removed

With a new release of MLAPI in Unity, some features have been removed:

- SyncVars have been removed from MLAPI. Use `NetworkVariable`s in place of this functionality. <!-- MTT54 -->
- [GitHub 527](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/527): Lag compensation systems and `TrackedObject` have moved to the new [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) repo.
- [GitHub 509](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/509): Encryption has been removed from MLAPI. The `Encryption` option in `NetworkConfig` on the `NetworkingManager` is not available in this release. This change will not block game creation or running. A current replacement for this functionality is not available, and may be developed in future releases. See the following changes:
    - Removed `SecuritySendFlags` from all APIs.
    - Removed encryption, cryptography, and certificate configurations from APIs including `NetworkManager` and `NetworkConfig`.
    - Removed "hail handshake", including `NetworkManager` implementation and `NetworkConstants` entries.
    - Modified `RpcQueue` and `RpcBatcher` internals to remove encryption and authentication from reading and writing.
- Removed the previous MLAPI Profiler editor window from Unity versions 2020.2 and later.
- Removed previous MLAPI Convenience and Performance RPC APIs with the new standard RPC API. See [RFC #1](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0001-std-rpc-api.md) for details.
- [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): Removed the MLAPI Installer.

### Known Issues

- `NetworkNavMeshAgent` does not synchronize mesh data, Agent Size, Steering, Obstacle Avoidance, or Path Finding settings. It only synchronizes the destination and velocity, not the path to the destination.
- For `RPC`, methods with a `ClientRpc` or `ServerRpc` suffix which are not marked with [ServerRpc] or [ClientRpc] will cause a compiler error.
- For `NetworkAnimator`, Animator Overrides are not supported. Triggers do not work.
- For `NetworkVariable`, the `NetworkDictionary` `List` and `Set` must use the `reliableSequenced` channel.
- `NetworkObjects`s are supported but when spawning a prefab with nested child network objects you have to manually call spawn on them
- `NetworkTransform` have the following issues:
  - Replicated objects may have jitter.
  - The owner is always authoritative about the object's position.
  - Scale is not synchronized.
- Connection Approval is not called on the host client.
- For `NamedMessages`, always use `NetworkBuffer` as the underlying stream for sending named and unnamed messages.
- For `NetworkManager`, connection management is limited. Use `IsServer`, `IsClient`, `IsConnectedClient`, or other code to check if MLAPI connected correctly.

## [0.0.1-preview.1] - 2020-12-20

This was an internally-only-used version of the Unity MLAPI Package
This commit is contained in:
Unity Technologies
2020-12-20 00:00:00 +00:00
commit 22d877d1b2
489 changed files with 43246 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
using System.Runtime.CompilerServices;
#if UNITY_EDITOR
[assembly: InternalsVisibleTo("Unity.Netcode.EditorTests")]
[assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")]
[assembly: InternalsVisibleTo("Unity.Netcode.Editor")]
[assembly: InternalsVisibleTo("TestProject.EditorTests")]
[assembly: InternalsVisibleTo("TestProject.RuntimeTests")]
#endif
[assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5b8086dc75d86473f9e3c928dd773733
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8eb56856ab05d41fa9e422a92acbc109
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Netcode
{
/// <summary>
/// Solves for incoming values that are jittered
/// Partially solves for message loss. Unclamped lerping helps hide this, but not completely
/// </summary>
/// <typeparam name="T"></typeparam>
internal abstract class BufferedLinearInterpolator<T> where T : struct
{
private struct BufferedItem
{
public T Item;
public double TimeSent;
public BufferedItem(T item, double timeSent)
{
Item = item;
TimeSent = timeSent;
}
}
private const double k_SmallValue = 9.999999439624929E-11; // copied from Vector3's equal operator
private T m_InterpStartValue;
private T m_CurrentInterpValue;
private T m_InterpEndValue;
private double m_EndTimeConsumed;
private double m_StartTimeConsumed;
private readonly List<BufferedItem> m_Buffer = new List<BufferedItem>(k_BufferCountLimit);
// Buffer consumption scenarios
// Perfect case consumption
// | 1 | 2 | 3 |
// | 2 | 3 | 4 | consume 1
// | 3 | 4 | 5 | consume 2
// | 4 | 5 | 6 | consume 3
// | 5 | 6 | 7 | consume 4
// jittered case
// | 1 | 2 | 3 |
// | 2 | 3 | | consume 1
// | 3 | | | consume 2
// | 4 | 5 | 6 | consume 3
// | 5 | 6 | 7 | consume 4
// bursted case (assuming max count is 5)
// | 1 | 2 | 3 |
// | 2 | 3 | | consume 1
// | 3 | | | consume 2
// | | | | consume 3
// | | | |
// | 4 | 5 | 6 | 7 | 8 | --> consume all and teleport to last value <8> --> this is the nuclear option, ideally this example would consume 4 and 5
// instead of jumping to 8, but since in OnValueChange we don't yet have an updated server time (updated in pre-update) to know which value
// we should keep and which we should drop, we don't have enough information to do this. Another thing would be to not have the burst in the first place.
// Constant absolute value for max buffer count instead of dynamic time based value. This is in case we have very low tick rates, so
// that we don't have a very small buffer because of this.
private const int k_BufferCountLimit = 100;
private BufferedItem m_LastBufferedItemReceived;
private int m_NbItemsReceivedThisFrame;
private int m_LifetimeConsumedCount;
private bool InvalidState => m_Buffer.Count == 0 && m_LifetimeConsumedCount == 0;
public void ResetTo(T targetValue, double serverTime)
{
m_LifetimeConsumedCount = 1;
m_InterpStartValue = targetValue;
m_InterpEndValue = targetValue;
m_CurrentInterpValue = targetValue;
m_Buffer.Clear();
m_EndTimeConsumed = 0.0d;
m_StartTimeConsumed = 0.0d;
Update(0, serverTime, serverTime);
}
// todo if I have value 1, 2, 3 and I'm treating 1 to 3, I shouldn't interpolate between 1 and 3, I should interpolate from 1 to 2, then from 2 to 3 to get the best path
private void TryConsumeFromBuffer(double renderTime, double serverTime)
{
int consumedCount = 0;
// only consume if we're ready
// this operation was measured as one of our most expensive, and we should put some thought into this.
// NetworkTransform has (currently) 7 buffered linear interpolators (3 position, 3 scale, 1 rot), and
// each has its own independent buffer and 'm_endTimeConsume'. That means every frame I have to do 7x
// these checks vs. if we tracked these values in a unified way
if (renderTime >= m_EndTimeConsumed)
{
BufferedItem? itemToInterpolateTo = null;
// assumes we're using sequenced messages for netvar syncing
// buffer contains oldest values first, iterating from end to start to remove elements from list while iterating
// calling m_Buffer.Count shows up hot in the profiler.
for (int i = m_Buffer.Count - 1; i >= 0; i--) // todo stretch: consume ahead if we see we're missing values due to packet loss
{
var bufferedValue = m_Buffer[i];
// Consume when ready and interpolate to last value we can consume. This can consume multiple values from the buffer
if (bufferedValue.TimeSent <= serverTime)
{
if (!itemToInterpolateTo.HasValue || bufferedValue.TimeSent > itemToInterpolateTo.Value.TimeSent)
{
if (m_LifetimeConsumedCount == 0)
{
// if interpolator not initialized, teleport to first value when available
m_StartTimeConsumed = bufferedValue.TimeSent;
m_InterpStartValue = bufferedValue.Item;
}
else if (consumedCount == 0)
{
// Interpolating to new value, end becomes start. We then look in our buffer for a new end.
m_StartTimeConsumed = m_EndTimeConsumed;
m_InterpStartValue = m_InterpEndValue;
}
if (bufferedValue.TimeSent > m_EndTimeConsumed)
{
itemToInterpolateTo = bufferedValue;
m_EndTimeConsumed = bufferedValue.TimeSent;
m_InterpEndValue = bufferedValue.Item;
}
}
m_Buffer.RemoveAt(i);
consumedCount++;
m_LifetimeConsumedCount++;
}
}
}
}
/// <summary>
/// Convenience version of 'Update' mainly for testing
/// the reason we don't want to always call this version is so that on the calling side we can compute
/// the renderTime once for the many things being interpolated (and the many interpolators per object)
/// </summary>
/// <param name="deltaTime">time since call</param>
/// <param name="serverTime">current server time</param>
public T Update(float deltaTime, NetworkTime serverTime)
{
return Update(deltaTime, serverTime.TimeTicksAgo(1).Time, serverTime.Time);
}
/// <summary>
/// Call to update the state of the interpolators before reading out
/// </summary>
/// <param name="deltaTime">time since last call</param>
/// <param name="renderTime">our current time</param>
/// <param name="serverTime">current server time</param>
public T Update(float deltaTime, double renderTime, double serverTime)
{
TryConsumeFromBuffer(renderTime, serverTime);
if (InvalidState)
{
throw new InvalidOperationException("trying to update interpolator when no data has been added to it yet");
}
// Interpolation example to understand the math below
// 4 4.5 6 6.5
// | | | |
// A render B Server
if (m_LifetimeConsumedCount >= 1) // shouldn't interpolate between default values, let's wait to receive data first, should only interpolate between real measurements
{
float t = 1.0f;
double range = m_EndTimeConsumed - m_StartTimeConsumed;
if (range > k_SmallValue)
{
t = (float)((renderTime - m_StartTimeConsumed) / range);
if (t < 0.0f)
{
throw new OverflowException($"t = {t} but must be >= 0. range {range}, RenderTime {renderTime}, Start time {m_StartTimeConsumed}, end time {m_EndTimeConsumed}");
}
if (t > 3.0f) // max extrapolation
{
// TODO this causes issues with teleport, investigate
// todo make this configurable
t = 1.0f;
}
}
var target = InterpolateUnclamped(m_InterpStartValue, m_InterpEndValue, t);
float maxInterpTime = 0.1f;
m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / maxInterpTime); // second interpolate to smooth out extrapolation jumps
}
m_NbItemsReceivedThisFrame = 0;
return m_CurrentInterpValue;
}
public void AddMeasurement(T newMeasurement, double sentTime)
{
m_NbItemsReceivedThisFrame++;
// This situation can happen after a game is paused. When starting to receive again, the server will have sent a bunch of messages in the meantime
// instead of going through thousands of value updates just to get a big teleport, we're giving up on interpolation and teleporting to the latest value
if (m_NbItemsReceivedThisFrame > k_BufferCountLimit)
{
if (m_LastBufferedItemReceived.TimeSent < sentTime)
{
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
ResetTo(newMeasurement, sentTime);
}
return;
}
if (sentTime > m_EndTimeConsumed || m_LifetimeConsumedCount == 0) // treat only if value is newer than the one being interpolated to right now
{
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
m_Buffer.Add(m_LastBufferedItemReceived);
}
}
public T GetInterpolatedValue()
{
return m_CurrentInterpValue;
}
protected abstract T Interpolate(T start, T end, float time);
protected abstract T InterpolateUnclamped(T start, T end, float time);
}
internal class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator<float>
{
protected override float InterpolateUnclamped(float start, float end, float time)
{
return Mathf.LerpUnclamped(start, end, time);
}
protected override float Interpolate(float start, float end, float time)
{
return Mathf.Lerp(start, end, time);
}
}
internal class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator<Quaternion>
{
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
{
return Quaternion.SlerpUnclamped(start, end, time);
}
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
{
return Quaternion.SlerpUnclamped(start, end, time);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a12ebf95bdb4445d9a16e4b6adadb6aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,454 @@
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
namespace Unity.Netcode.Components
{
/// <summary>
/// A prototype component for syncing animations
/// </summary>
[AddComponentMenu("Netcode/" + nameof(NetworkAnimator))]
[RequireComponent(typeof(Animator))]
public class NetworkAnimator : NetworkBehaviour
{
internal struct AnimationMessage : INetworkSerializable
{
public int StateHash; // if non-zero, then Play() this animation, skipping transitions
public float NormalizedTime;
public byte[] Parameters;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref StateHash);
serializer.SerializeValue(ref NormalizedTime);
serializer.SerializeValue(ref Parameters);
}
}
internal struct AnimationParametersMessage : INetworkSerializable
{
public byte[] Parameters;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref Parameters);
}
}
internal struct AnimationTriggerMessage : INetworkSerializable
{
public int Hash;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref Hash);
}
}
[SerializeField] private Animator m_Animator;
[SerializeField] private uint m_ParameterSendBits;
[SerializeField] private float m_SendRate = 0.1f;
public Animator Animator
{
get { return m_Animator; }
set
{
m_Animator = value;
ResetParameterOptions();
}
}
/*
* AutoSend is the ability to select which parameters linked to this animator
* get replicated on a regular basis regardless of a state change. The thinking
* behind this is that many of the parameters people use are usually booleans
* which result in a state change and thus would cause a full sync of state.
* Thus if you really care about a parameter syncing then you need to be explict
* by selecting it in the inspector when an NetworkAnimator is selected.
*/
public void SetParameterAutoSend(int index, bool value)
{
if (value)
{
m_ParameterSendBits |= (uint)(1 << index);
}
else
{
m_ParameterSendBits &= (uint)(~(1 << index));
}
}
public bool GetParameterAutoSend(int index)
{
return (m_ParameterSendBits & (uint)(1 << index)) != 0;
}
// Animators only support up to 32 params
public static int K_MaxAnimationParams = 32;
private int m_TransitionHash;
private double m_NextSendTime = 0.0f;
private int m_AnimationHash;
public int AnimationHash { get => m_AnimationHash; }
private unsafe struct AnimatorParamCache
{
public int Hash;
public int Type;
public fixed byte Value[4]; // this is a max size of 4 bytes
}
// 128bytes per Animator
private FastBufferWriter m_ParameterWriter = new FastBufferWriter(K_MaxAnimationParams * sizeof(float), Allocator.Persistent);
private NativeArray<AnimatorParamCache> m_CachedAnimatorParameters;
// We cache these values because UnsafeUtility.EnumToInt use direct IL that allows a nonboxing conversion
private struct AnimationParamEnumWrapper
{
public static readonly int AnimatorControllerParameterInt;
public static readonly int AnimatorControllerParameterFloat;
public static readonly int AnimatorControllerParameterBool;
static AnimationParamEnumWrapper()
{
AnimatorControllerParameterInt = UnsafeUtility.EnumToInt(AnimatorControllerParameterType.Int);
AnimatorControllerParameterFloat = UnsafeUtility.EnumToInt(AnimatorControllerParameterType.Float);
AnimatorControllerParameterBool = UnsafeUtility.EnumToInt(AnimatorControllerParameterType.Bool);
}
}
internal void ResetParameterOptions()
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfoServer("ResetParameterOptions");
}
m_ParameterSendBits = 0;
}
private bool sendMessagesAllowed
{
get
{
return IsServer && NetworkObject.IsSpawned;
}
}
public override void OnDestroy()
{
if (m_CachedAnimatorParameters.IsCreated)
{
m_CachedAnimatorParameters.Dispose();
}
m_ParameterWriter.Dispose();
}
public override void OnNetworkSpawn()
{
var parameters = m_Animator.parameters;
m_CachedAnimatorParameters = new NativeArray<AnimatorParamCache>(parameters.Length, Allocator.Persistent);
m_AnimationHash = -1;
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
if (m_Animator.IsParameterControlledByCurve(parameter.nameHash))
{
//we are ignoring parameters that are controlled by animation curves - syncing the layer states indirectly syncs the values that are driven by the animation curves
continue;
}
var cacheParam = new AnimatorParamCache();
cacheParam.Type = UnsafeUtility.EnumToInt(parameter.type);
cacheParam.Hash = parameter.nameHash;
unsafe
{
switch (parameter.type)
{
case AnimatorControllerParameterType.Float:
var value = m_Animator.GetFloat(cacheParam.Hash);
UnsafeUtility.WriteArrayElement(cacheParam.Value, 0, value);
break;
case AnimatorControllerParameterType.Int:
var valueInt = m_Animator.GetInteger(cacheParam.Hash);
UnsafeUtility.WriteArrayElement(cacheParam.Value, 0, valueInt);
break;
case AnimatorControllerParameterType.Bool:
var valueBool = m_Animator.GetBool(cacheParam.Hash);
UnsafeUtility.WriteArrayElement(cacheParam.Value, 0, valueBool);
break;
case AnimatorControllerParameterType.Trigger:
default:
break;
}
}
m_CachedAnimatorParameters[i] = cacheParam;
}
}
private void FixedUpdate()
{
if (!sendMessagesAllowed)
{
return;
}
int stateHash;
float normalizedTime;
if (!CheckAnimStateChanged(out stateHash, out normalizedTime))
{
// We only want to check and send if we don't have any other state to since
// as we will sync all params as part of the state sync
CheckAndSend();
return;
}
var animMsg = new AnimationMessage();
animMsg.StateHash = stateHash;
animMsg.NormalizedTime = normalizedTime;
m_ParameterWriter.Seek(0);
m_ParameterWriter.Truncate();
WriteParameters(m_ParameterWriter, false);
animMsg.Parameters = m_ParameterWriter.ToArray();
SendAnimStateClientRpc(animMsg);
}
private void CheckAndSend()
{
var networkTime = NetworkManager.ServerTime.Time;
if (sendMessagesAllowed && m_SendRate != 0 && m_NextSendTime < networkTime)
{
m_NextSendTime = networkTime + m_SendRate;
m_ParameterWriter.Seek(0);
m_ParameterWriter.Truncate();
if (WriteParameters(m_ParameterWriter, true))
{
// we then sync the params we care about
var animMsg = new AnimationParametersMessage()
{
Parameters = m_ParameterWriter.ToArray()
};
SendParamsClientRpc(animMsg);
}
}
}
private bool CheckAnimStateChanged(out int stateHash, out float normalizedTime)
{
stateHash = 0;
normalizedTime = 0;
if (m_Animator.IsInTransition(0))
{
AnimatorTransitionInfo tt = m_Animator.GetAnimatorTransitionInfo(0);
if (tt.fullPathHash != m_TransitionHash)
{
// first time in this transition
m_TransitionHash = tt.fullPathHash;
m_AnimationHash = 0;
return true;
}
return false;
}
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(0);
if (st.fullPathHash != m_AnimationHash)
{
// first time in this animation state
if (m_AnimationHash != 0)
{
// came from another animation directly - from Play()
stateHash = st.fullPathHash;
normalizedTime = st.normalizedTime;
}
m_TransitionHash = 0;
m_AnimationHash = st.fullPathHash;
return true;
}
return false;
}
private unsafe bool WriteParameters(FastBufferWriter writer, bool autoSend)
{
if (m_CachedAnimatorParameters == null)
{
return false;
}
for (int i = 0; i < m_CachedAnimatorParameters.Length; i++)
{
if (autoSend && !GetParameterAutoSend(i))
{
continue;
}
ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef<AnimatorParamCache>(m_CachedAnimatorParameters.GetUnsafePtr(), i);
var hash = cacheValue.Hash;
if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterInt)
{
var valueInt = m_Animator.GetInteger(hash);
fixed (void* value = cacheValue.Value)
{
var oldValue = UnsafeUtility.AsRef<int>(value);
if (valueInt != oldValue)
{
UnsafeUtility.WriteArrayElement(value, 0, valueInt);
BytePacker.WriteValuePacked(writer, (uint)valueInt);
}
}
}
else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterBool)
{
var valueBool = m_Animator.GetBool(hash);
fixed (void* value = cacheValue.Value)
{
var oldValue = UnsafeUtility.AsRef<bool>(value);
if (valueBool != oldValue)
{
UnsafeUtility.WriteArrayElement(value, 0, valueBool);
writer.WriteValueSafe(valueBool);
}
}
}
else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat)
{
var valueFloat = m_Animator.GetFloat(hash);
fixed (void* value = cacheValue.Value)
{
var oldValue = UnsafeUtility.AsRef<float>(value);
if (valueFloat != oldValue)
{
UnsafeUtility.WriteArrayElement(value, 0, valueFloat);
writer.WriteValueSafe(valueFloat);
}
}
}
}
// If we do not write any values to the writer then we should not send any data
return writer.Length > 0;
}
private unsafe void ReadParameters(FastBufferReader reader, bool autoSend)
{
if (m_CachedAnimatorParameters == null)
{
return;
}
for (int i = 0; i < m_CachedAnimatorParameters.Length; i++)
{
if (autoSend && !GetParameterAutoSend(i))
{
continue;
}
ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef<AnimatorParamCache>(m_CachedAnimatorParameters.GetUnsafePtr(), i);
var hash = cacheValue.Hash;
if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterInt)
{
ByteUnpacker.ReadValuePacked(reader, out int newValue);
m_Animator.SetInteger(hash, newValue);
fixed (void* value = cacheValue.Value)
{
UnsafeUtility.WriteArrayElement(value, 0, newValue);
}
}
else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterBool)
{
reader.ReadValueSafe(out bool newBoolValue);
m_Animator.SetBool(hash, newBoolValue);
fixed (void* value = cacheValue.Value)
{
UnsafeUtility.WriteArrayElement(value, 0, newBoolValue);
}
}
else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat)
{
reader.ReadValueSafe(out float newFloatValue);
m_Animator.SetFloat(hash, newFloatValue);
fixed (void* value = cacheValue.Value)
{
UnsafeUtility.WriteArrayElement(value, 0, newFloatValue);
}
}
}
}
[ClientRpc]
private unsafe void SendParamsClientRpc(AnimationParametersMessage animSnapshot, ClientRpcParams clientRpcParams = default)
{
if (animSnapshot.Parameters != null)
{
// We use a fixed value here to avoid the copy of data from the byte buffer since we own the data
fixed (byte* parameters = animSnapshot.Parameters)
{
var reader = new FastBufferReader(parameters, Allocator.None, animSnapshot.Parameters.Length);
ReadParameters(reader, true);
}
}
}
[ClientRpc]
private unsafe void SendAnimStateClientRpc(AnimationMessage animSnapshot, ClientRpcParams clientRpcParams = default)
{
if (animSnapshot.StateHash != 0)
{
m_AnimationHash = animSnapshot.StateHash;
m_Animator.Play(animSnapshot.StateHash, 0, animSnapshot.NormalizedTime);
}
if (animSnapshot.Parameters != null && animSnapshot.Parameters.Length != 0)
{
// We use a fixed value here to avoid the copy of data from the byte buffer since we own the data
fixed (byte* parameters = animSnapshot.Parameters)
{
var reader = new FastBufferReader(parameters, Allocator.None, animSnapshot.Parameters.Length);
ReadParameters(reader, false);
}
}
}
[ClientRpc]
private void SendAnimTriggerClientRpc(AnimationTriggerMessage animSnapshot, ClientRpcParams clientRpcParams = default)
{
m_Animator.SetTrigger(animSnapshot.Hash);
}
public void SetTrigger(string triggerName)
{
SetTrigger(Animator.StringToHash(triggerName));
}
public void SetTrigger(int hash)
{
var animMsg = new AnimationTriggerMessage();
animMsg.Hash = hash;
if (IsServer)
{
SendAnimTriggerClientRpc(animMsg);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8d0727d5ae3244e3b569694d3912374
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
using UnityEngine;
namespace Unity.Netcode.Components
{
/// <summary>
/// NetworkRigidbody allows for the use of <see cref="Rigidbody"/> on network objects. By controlling the kinematic
/// mode of the rigidbody and disabling it on all peers but the authoritative one.
/// </summary>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(NetworkTransform))]
public class NetworkRigidbody : NetworkBehaviour
{
private Rigidbody m_Rigidbody;
private NetworkTransform m_NetworkTransform;
private bool m_OriginalKinematic;
private RigidbodyInterpolation m_OriginalInterpolation;
// Used to cache the authority state of this rigidbody during the last frame
private bool m_IsAuthority;
/// <summary>
/// Gets a bool value indicating whether this <see cref="NetworkRigidbody"/> on this peer currently holds authority.
/// </summary>
private bool HasAuthority => m_NetworkTransform.CanCommitToTransform;
private void Awake()
{
m_Rigidbody = GetComponent<Rigidbody>();
m_NetworkTransform = GetComponent<NetworkTransform>();
}
private void FixedUpdate()
{
if (NetworkManager.IsListening)
{
if (HasAuthority != m_IsAuthority)
{
m_IsAuthority = HasAuthority;
UpdateRigidbodyKinematicMode();
}
}
}
// Puts the rigidbody in a kinematic non-interpolated mode on everyone but the server.
private void UpdateRigidbodyKinematicMode()
{
if (m_IsAuthority == false)
{
m_OriginalKinematic = m_Rigidbody.isKinematic;
m_Rigidbody.isKinematic = true;
m_OriginalInterpolation = m_Rigidbody.interpolation;
// Set interpolation to none, the NetworkTransform component interpolates the position of the object.
m_Rigidbody.interpolation = RigidbodyInterpolation.None;
}
else
{
// Resets the rigidbody back to it's non replication only state. Happens on shutdown and when authority is lost
m_Rigidbody.isKinematic = m_OriginalKinematic;
m_Rigidbody.interpolation = m_OriginalInterpolation;
}
}
/// <inheritdoc />
public override void OnNetworkSpawn()
{
m_IsAuthority = HasAuthority;
m_OriginalKinematic = m_Rigidbody.isKinematic;
m_OriginalInterpolation = m_Rigidbody.interpolation;
UpdateRigidbodyKinematicMode();
}
/// <inheritdoc />
public override void OnNetworkDespawn()
{
UpdateRigidbodyKinematicMode();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6c0be61502bb534f922ebb746851216
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
using UnityEngine;
namespace Unity.Netcode.Components
{
/// <summary>
/// NetworkRigidbody allows for the use of <see cref="Rigidbody2D"/> on network objects. By controlling the kinematic
/// mode of the rigidbody and disabling it on all peers but the authoritative one.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(NetworkTransform))]
public class NetworkRigidbody2D : NetworkBehaviour
{
private Rigidbody2D m_Rigidbody;
private NetworkTransform m_NetworkTransform;
private bool m_OriginalKinematic;
private RigidbodyInterpolation2D m_OriginalInterpolation;
// Used to cache the authority state of this rigidbody during the last frame
private bool m_IsAuthority;
/// <summary>
/// Gets a bool value indicating whether this <see cref="NetworkRigidbody2D"/> on this peer currently holds authority.
/// </summary>
private bool HasAuthority => m_NetworkTransform.CanCommitToTransform;
private void Awake()
{
m_Rigidbody = GetComponent<Rigidbody2D>();
m_NetworkTransform = GetComponent<NetworkTransform>();
}
private void FixedUpdate()
{
if (NetworkManager.IsListening)
{
if (HasAuthority != m_IsAuthority)
{
m_IsAuthority = HasAuthority;
UpdateRigidbodyKinematicMode();
}
}
}
// Puts the rigidbody in a kinematic non-interpolated mode on everyone but the server.
private void UpdateRigidbodyKinematicMode()
{
if (m_IsAuthority == false)
{
m_OriginalKinematic = m_Rigidbody.isKinematic;
m_Rigidbody.isKinematic = true;
m_OriginalInterpolation = m_Rigidbody.interpolation;
// Set interpolation to none, the NetworkTransform component interpolates the position of the object.
m_Rigidbody.interpolation = RigidbodyInterpolation2D.None;
}
else
{
// Resets the rigidbody back to it's non replication only state. Happens on shutdown and when authority is lost
m_Rigidbody.isKinematic = m_OriginalKinematic;
m_Rigidbody.interpolation = m_OriginalInterpolation;
}
}
/// <inheritdoc />
public override void OnNetworkSpawn()
{
m_IsAuthority = HasAuthority;
m_OriginalKinematic = m_Rigidbody.isKinematic;
m_OriginalInterpolation = m_Rigidbody.interpolation;
UpdateRigidbodyKinematicMode();
}
/// <inheritdoc />
public override void OnNetworkDespawn()
{
m_IsAuthority = false;
UpdateRigidbodyKinematicMode();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 80d7c879794dfda4687da0e400131852
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,900 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Unity.Netcode.Components
{
/// <summary>
/// A component for syncing transforms
/// NetworkTransform will read the underlying transform and replicate it to clients.
/// The replicated value will be automatically be interpolated (if active) and applied to the underlying GameObject's transform
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("Netcode/" + nameof(NetworkTransform))]
[DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts
public class NetworkTransform : NetworkBehaviour
{
public const float PositionThresholdDefault = .001f;
public const float RotAngleThresholdDefault = .01f;
public const float ScaleThresholdDefault = .01f;
public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale);
public OnClientRequestChangeDelegate OnClientRequestChange;
internal struct NetworkTransformState : INetworkSerializable
{
private const int k_InLocalSpaceBit = 0;
private const int k_PositionXBit = 1;
private const int k_PositionYBit = 2;
private const int k_PositionZBit = 3;
private const int k_RotAngleXBit = 4;
private const int k_RotAngleYBit = 5;
private const int k_RotAngleZBit = 6;
private const int k_ScaleXBit = 7;
private const int k_ScaleYBit = 8;
private const int k_ScaleZBit = 9;
private const int k_TeleportingBit = 10;
// 11-15: <unused>
private ushort m_Bitset;
public bool InLocalSpace
{
get => (m_Bitset & (1 << k_InLocalSpaceBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_InLocalSpaceBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_InLocalSpaceBit)); }
}
}
// Position
public bool HasPositionX
{
get => (m_Bitset & (1 << k_PositionXBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionXBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionXBit)); }
}
}
public bool HasPositionY
{
get => (m_Bitset & (1 << k_PositionYBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionYBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionYBit)); }
}
}
public bool HasPositionZ
{
get => (m_Bitset & (1 << k_PositionZBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionZBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionZBit)); }
}
}
// RotAngles
public bool HasRotAngleX
{
get => (m_Bitset & (1 << k_RotAngleXBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleXBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleXBit)); }
}
}
public bool HasRotAngleY
{
get => (m_Bitset & (1 << k_RotAngleYBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleYBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleYBit)); }
}
}
public bool HasRotAngleZ
{
get => (m_Bitset & (1 << k_RotAngleZBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleZBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleZBit)); }
}
}
// Scale
public bool HasScaleX
{
get => (m_Bitset & (1 << k_ScaleXBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleXBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleXBit)); }
}
}
public bool HasScaleY
{
get => (m_Bitset & (1 << k_ScaleYBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleYBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleYBit)); }
}
}
public bool HasScaleZ
{
get => (m_Bitset & (1 << k_ScaleZBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleZBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleZBit)); }
}
}
public bool IsTeleportingNextFrame
{
get => (m_Bitset & (1 << k_TeleportingBit)) != 0;
set
{
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_TeleportingBit)); }
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_TeleportingBit)); }
}
}
public float PositionX, PositionY, PositionZ;
public float RotAngleX, RotAngleY, RotAngleZ;
public float ScaleX, ScaleY, ScaleZ;
public double SentTime;
public Vector3 Position
{
get { return new Vector3(PositionX, PositionY, PositionZ); }
set
{
PositionX = value.x;
PositionY = value.y;
PositionZ = value.z;
}
}
public Vector3 Rotation
{
get { return new Vector3(RotAngleX, RotAngleY, RotAngleZ); }
set
{
RotAngleX = value.x;
RotAngleY = value.y;
RotAngleZ = value.z;
}
}
public Vector3 Scale
{
get { return new Vector3(ScaleX, ScaleY, ScaleZ); }
set
{
ScaleX = value.x;
ScaleY = value.y;
ScaleZ = value.z;
}
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref SentTime);
// InLocalSpace + HasXXX Bits
serializer.SerializeValue(ref m_Bitset);
// Position Values
if (HasPositionX)
{
serializer.SerializeValue(ref PositionX);
}
if (HasPositionY)
{
serializer.SerializeValue(ref PositionY);
}
if (HasPositionZ)
{
serializer.SerializeValue(ref PositionZ);
}
// RotAngle Values
if (HasRotAngleX)
{
serializer.SerializeValue(ref RotAngleX);
}
if (HasRotAngleY)
{
serializer.SerializeValue(ref RotAngleY);
}
if (HasRotAngleZ)
{
serializer.SerializeValue(ref RotAngleZ);
}
// Scale Values
if (HasScaleX)
{
serializer.SerializeValue(ref ScaleX);
}
if (HasScaleY)
{
serializer.SerializeValue(ref ScaleY);
}
if (HasScaleZ)
{
serializer.SerializeValue(ref ScaleZ);
}
}
}
public bool SyncPositionX = true, SyncPositionY = true, SyncPositionZ = true;
public bool SyncRotAngleX = true, SyncRotAngleY = true, SyncRotAngleZ = true;
public bool SyncScaleX = true, SyncScaleY = true, SyncScaleZ = true;
public float PositionThreshold = PositionThresholdDefault;
public float RotAngleThreshold = RotAngleThresholdDefault;
public float ScaleThreshold = ScaleThresholdDefault;
/// <summary>
/// Sets whether this transform should sync in local space or in world space.
/// This is important to set since reparenting this transform could have issues,
/// if using world position (depending on who gets synced first: the parent or the child)
/// Having a child always at position 0,0,0 for example will have less possibilities of desync than when using world positions
/// </summary>
[Tooltip("Sets whether this transform should sync in local space or in world space")]
public bool InLocalSpace = false;
public bool Interpolate = true;
/// <summary>
/// Used to determine who can write to this transform. Server only for this transform.
/// Changing this value alone in a child implementation will not allow you to create a NetworkTransform which can be written to by clients. See the ClientNetworkTransform Sample
/// in the package samples for how to implement a NetworkTransform with client write support.
/// If using different values, please use RPCs to write to the server. Netcode doesn't support client side network variable writing
/// </summary>
// This is public to make sure that users don't depend on this IsClient && IsOwner check in their code. If this logic changes in the future, we can make it invisible here
public bool CanCommitToTransform;
protected bool m_CachedIsServer;
protected NetworkManager m_CachedNetworkManager;
private readonly NetworkVariable<NetworkTransformState> m_ReplicatedNetworkState = new NetworkVariable<NetworkTransformState>(new NetworkTransformState());
private NetworkTransformState m_LocalAuthoritativeNetworkState;
private NetworkTransformState m_PrevNetworkState;
private const int k_DebugDrawLineTime = 10;
private bool m_HasSentLastValue = false; // used to send one last value, so clients can make the difference between lost replication data (clients extrapolate) and no more data to send.
private BufferedLinearInterpolator<float> m_PositionXInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator<float> m_PositionYInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator<float> m_PositionZInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator<Quaternion> m_RotationInterpolator; // = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value
private BufferedLinearInterpolator<float> m_ScaleXInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator<float> m_ScaleYInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator<float> m_ScaleZInterpolator; // = new BufferedLinearInterpolatorFloat();
private readonly List<BufferedLinearInterpolator<float>> m_AllFloatInterpolators = new List<BufferedLinearInterpolator<float>>(6);
private Transform m_Transform; // cache the transform component to reduce unnecessary bounce between managed and native
private int m_LastSentTick;
private NetworkTransformState m_LastSentState;
/// <summary>
/// Tries updating the server authoritative transform, only if allowed.
/// If this called server side, this will commit directly.
/// If no update is needed, nothing will be sent. This method should still be called every update, it'll self manage when it should and shouldn't send
/// </summary>
/// <param name="transformToCommit"></param>
/// <param name="dirtyTime"></param>
protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime)
{
var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit);
TryCommit(isDirty);
}
private void TryCommitValuesToServer(Vector3 position, Vector3 rotation, Vector3 scale, double dirtyTime)
{
var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, dirtyTime, position, rotation, scale);
TryCommit(isDirty.isDirty);
}
private void TryCommit(bool isDirty)
{
void Send(NetworkTransformState stateToSend)
{
if (m_CachedIsServer)
{
// server RPC takes a few frames to execute server side, we want this to execute immediately
CommitLocallyAndReplicate(stateToSend);
}
else
{
CommitTransformServerRpc(stateToSend);
}
}
// if dirty, send
// if not dirty anymore, but hasn't sent last value for limiting extrapolation, still set isDirty
// if not dirty and has already sent last value, don't do anything
// extrapolation works by using last two values. if it doesn't receive anything anymore, it'll continue to extrapolate.
// This is great in case there's message loss, not so great if we just don't have new values to send.
// the following will send one last "copied" value so unclamped interpolation tries to extrapolate between two identical values, effectively
// making it immobile.
if (isDirty)
{
Send(m_LocalAuthoritativeNetworkState);
m_HasSentLastValue = false;
m_LastSentTick = m_CachedNetworkManager.LocalTime.Tick;
m_LastSentState = m_LocalAuthoritativeNetworkState;
}
else if (!m_HasSentLastValue && m_CachedNetworkManager.LocalTime.Tick >= m_LastSentTick + 1) // check for state.IsDirty since update can happen more than once per tick. No need for client, RPCs will just queue up
{
m_LastSentState.SentTime = m_CachedNetworkManager.LocalTime.Time; // time 1+ tick later
Send(m_LastSentState);
m_HasSentLastValue = true;
}
}
[ServerRpc(RequireOwnership = false)]
private void CommitTransformServerRpc(NetworkTransformState networkState, ServerRpcParams serverParams = default)
{
if (serverParams.Receive.SenderClientId == OwnerClientId) // RPC call when not authorized to write could happen during the RTT interval during which a server's ownership change hasn't reached the client yet
{
CommitLocallyAndReplicate(networkState);
}
}
private void CommitLocallyAndReplicate(NetworkTransformState networkState)
{
m_ReplicatedNetworkState.Value = networkState;
AddInterpolatedState(networkState);
}
private void ResetInterpolatedStateToCurrentAuthoritativeState()
{
var serverTime = NetworkManager.ServerTime.Time;
m_PositionXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionX, serverTime);
m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime);
m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime);
m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.Rotation), serverTime);
m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime);
m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime);
m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime);
}
// updates `NetworkState` properties if they need to and returns a `bool` indicating whether or not there was any changes made
// returned boolean would be useful to change encapsulating `NetworkVariable<NetworkState>`'s dirty state, e.g. ReplNetworkState.SetDirty(isDirty);
internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
{
return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, transformToUse).isDirty;
}
private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
{
var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position;
var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles;
var scale = transformToUse.localScale;
return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, position, rotAngles, scale);
}
private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Vector3 position, Vector3 rotAngles, Vector3 scale)
{
var isDirty = false;
var isPositionDirty = false;
var isRotationDirty = false;
var isScaleDirty = false;
// hasPositionZ set to false when it should be true?
if (InLocalSpace != networkState.InLocalSpace)
{
networkState.InLocalSpace = InLocalSpace;
isDirty = true;
}
// we assume that if x, y or z are dirty then we'll have to send all 3 anyway, so for efficiency
// we skip doing the (quite expensive) Math.Approximately() and check against PositionThreshold
// this still is overly costly and could use more improvements.
//
// (ditto for scale components)
if (SyncPositionX &&
Mathf.Abs(networkState.PositionX - position.x) > PositionThreshold)
{
networkState.PositionX = position.x;
networkState.HasPositionX = true;
isPositionDirty = true;
}
if (SyncPositionY &&
Mathf.Abs(networkState.PositionY - position.y) > PositionThreshold)
{
networkState.PositionY = position.y;
networkState.HasPositionY = true;
isPositionDirty = true;
}
if (SyncPositionZ &&
Mathf.Abs(networkState.PositionZ - position.z) > PositionThreshold)
{
networkState.PositionZ = position.z;
networkState.HasPositionZ = true;
isPositionDirty = true;
}
if (SyncRotAngleX &&
Mathf.Abs(networkState.RotAngleX - rotAngles.x) > RotAngleThreshold)
{
networkState.RotAngleX = rotAngles.x;
networkState.HasRotAngleX = true;
isRotationDirty = true;
}
if (SyncRotAngleY &&
Mathf.Abs(networkState.RotAngleY - rotAngles.y) > RotAngleThreshold)
{
networkState.RotAngleY = rotAngles.y;
networkState.HasRotAngleY = true;
isRotationDirty = true;
}
if (SyncRotAngleZ &&
Mathf.Abs(networkState.RotAngleZ - rotAngles.z) > RotAngleThreshold)
{
networkState.RotAngleZ = rotAngles.z;
networkState.HasRotAngleZ = true;
isRotationDirty = true;
}
if (SyncScaleX &&
Mathf.Abs(networkState.ScaleX - scale.x) > ScaleThreshold)
{
networkState.ScaleX = scale.x;
networkState.HasScaleX = true;
isScaleDirty = true;
}
if (SyncScaleY &&
Mathf.Abs(networkState.ScaleY - scale.y) > ScaleThreshold)
{
networkState.ScaleY = scale.y;
networkState.HasScaleY = true;
isScaleDirty = true;
}
if (SyncScaleZ &&
Mathf.Abs(networkState.ScaleZ - scale.z) > ScaleThreshold)
{
networkState.ScaleZ = scale.z;
networkState.HasScaleZ = true;
isScaleDirty = true;
}
isDirty |= isPositionDirty || isRotationDirty || isScaleDirty;
if (isDirty)
{
networkState.SentTime = dirtyTime;
}
return (isDirty, isPositionDirty, isRotationDirty, isScaleDirty);
}
private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState networkState, Transform transformToUpdate)
{
m_PrevNetworkState = networkState;
var interpolatedPosition = InLocalSpace ? transformToUpdate.localPosition : transformToUpdate.position;
// todo: we should store network state w/ quats vs. euler angles
var interpolatedRotAngles = InLocalSpace ? transformToUpdate.localEulerAngles : transformToUpdate.eulerAngles;
var interpolatedScale = transformToUpdate.localScale;
// InLocalSpace Read
InLocalSpace = networkState.InLocalSpace;
// Position Read
if (SyncPositionX)
{
interpolatedPosition.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.x : m_PositionXInterpolator.GetInterpolatedValue();
}
if (SyncPositionY)
{
interpolatedPosition.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.y : m_PositionYInterpolator.GetInterpolatedValue();
}
if (SyncPositionZ)
{
interpolatedPosition.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.z : m_PositionZInterpolator.GetInterpolatedValue();
}
// again, we should be using quats here
if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ)
{
var eulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles;
if (SyncRotAngleX)
{
interpolatedRotAngles.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.x : eulerAngles.x;
}
if (SyncRotAngleY)
{
interpolatedRotAngles.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.y : eulerAngles.y;
}
if (SyncRotAngleZ)
{
interpolatedRotAngles.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.z : eulerAngles.z;
}
}
// Scale Read
if (SyncScaleX)
{
interpolatedScale.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.x : m_ScaleXInterpolator.GetInterpolatedValue();
}
if (SyncScaleY)
{
interpolatedScale.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.y : m_ScaleYInterpolator.GetInterpolatedValue();
}
if (SyncScaleZ)
{
interpolatedScale.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.z : m_ScaleZInterpolator.GetInterpolatedValue();
}
// Position Apply
if (SyncPositionX || SyncPositionY || SyncPositionZ)
{
if (InLocalSpace)
{
transformToUpdate.localPosition = interpolatedPosition;
}
else
{
transformToUpdate.position = interpolatedPosition;
}
m_PrevNetworkState.Position = interpolatedPosition;
}
// RotAngles Apply
if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ)
{
if (InLocalSpace)
{
transformToUpdate.localRotation = Quaternion.Euler(interpolatedRotAngles);
}
else
{
transformToUpdate.rotation = Quaternion.Euler(interpolatedRotAngles);
}
m_PrevNetworkState.Rotation = interpolatedRotAngles;
}
// Scale Apply
if (SyncScaleX || SyncScaleY || SyncScaleZ)
{
transformToUpdate.localScale = interpolatedScale;
m_PrevNetworkState.Scale = interpolatedScale;
}
}
private void AddInterpolatedState(NetworkTransformState newState)
{
var sentTime = newState.SentTime;
if (newState.HasPositionX)
{
m_PositionXInterpolator.AddMeasurement(newState.PositionX, sentTime);
}
if (newState.HasPositionY)
{
m_PositionYInterpolator.AddMeasurement(newState.PositionY, sentTime);
}
if (newState.HasPositionZ)
{
m_PositionZInterpolator.AddMeasurement(newState.PositionZ, sentTime);
}
m_RotationInterpolator.AddMeasurement(Quaternion.Euler(newState.Rotation), sentTime);
if (newState.HasScaleX)
{
m_ScaleXInterpolator.AddMeasurement(newState.ScaleX, sentTime);
}
if (newState.HasScaleY)
{
m_ScaleYInterpolator.AddMeasurement(newState.ScaleY, sentTime);
}
if (newState.HasScaleZ)
{
m_ScaleZInterpolator.AddMeasurement(newState.ScaleZ, sentTime);
}
}
private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState)
{
if (!NetworkObject.IsSpawned)
{
// todo MTT-849 should never happen but yet it does! maybe revisit/dig after NetVar updates and snapshot system lands?
return;
}
if (CanCommitToTransform)
{
// we're the authority, we ignore incoming changes
return;
}
Debug.DrawLine(newState.Position, newState.Position + Vector3.up + Vector3.left, Color.green, 10, false);
AddInterpolatedState(newState);
if (m_CachedNetworkManager.LogLevel == LogLevel.Developer)
{
var pos = new Vector3(newState.PositionX, newState.PositionY, newState.PositionZ);
Debug.DrawLine(pos, pos + Vector3.up + Vector3.left * Random.Range(0.5f, 2f), Color.green, k_DebugDrawLineTime, false);
}
}
private void Awake()
{
// we only want to create our interpolators during Awake so that, when pooled, we do not create tons
// of gc thrash each time objects wink out and are re-used
m_PositionXInterpolator = new BufferedLinearInterpolatorFloat();
m_PositionYInterpolator = new BufferedLinearInterpolatorFloat();
m_PositionZInterpolator = new BufferedLinearInterpolatorFloat();
m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value
m_ScaleXInterpolator = new BufferedLinearInterpolatorFloat();
m_ScaleYInterpolator = new BufferedLinearInterpolatorFloat();
m_ScaleZInterpolator = new BufferedLinearInterpolatorFloat();
if (m_AllFloatInterpolators.Count == 0)
{
m_AllFloatInterpolators.Add(m_PositionXInterpolator);
m_AllFloatInterpolators.Add(m_PositionYInterpolator);
m_AllFloatInterpolators.Add(m_PositionZInterpolator);
m_AllFloatInterpolators.Add(m_ScaleXInterpolator);
m_AllFloatInterpolators.Add(m_ScaleYInterpolator);
m_AllFloatInterpolators.Add(m_ScaleZInterpolator);
}
}
public override void OnNetworkSpawn()
{
// must set up m_Transform in OnNetworkSpawn because it's possible an object spawns but is disabled
// and thus awake won't be called.
// TODO: investigate further on not sending data for something that is not enabled
m_Transform = transform;
m_ReplicatedNetworkState.OnValueChanged += OnNetworkStateChanged;
CanCommitToTransform = IsServer;
m_CachedIsServer = IsServer;
m_CachedNetworkManager = NetworkManager;
if (CanCommitToTransform)
{
TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time);
}
m_LocalAuthoritativeNetworkState = m_ReplicatedNetworkState.Value;
// crucial we do this to reset the interpolators so that recycled objects when using a pool will
// not have leftover interpolator state from the previous object
Initialize();
}
public override void OnNetworkDespawn()
{
m_ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged;
}
public override void OnGainedOwnership()
{
Initialize();
}
public override void OnLostOwnership()
{
Initialize();
}
private void Initialize()
{
ResetInterpolatedStateToCurrentAuthoritativeState(); // useful for late joining
if (CanCommitToTransform)
{
m_ReplicatedNetworkState.SetDirty(true);
}
else
{
ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform);
}
}
#region state set
/// <summary>
/// Directly sets a state on the authoritative transform.
/// This will override any changes made previously to the transform
/// This isn't resistant to network jitter. Server side changes due to this method won't be interpolated.
/// The parameters are broken up into pos / rot / scale on purpose so that the caller can perturb
/// just the desired one(s)
/// </summary>
/// <param name="posIn"></param> new position to move to. Can be null
/// <param name="rotIn"></param> new rotation to rotate to. Can be null
/// <param name="scaleIn">new scale to scale to. Can be null</param>
/// <param name="shouldGhostsInterpolate">Should other clients interpolate this change or not. True by default</param>
/// new scale to scale to. Can be null
/// <exception cref="Exception"></exception>
public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null, bool shouldGhostsInterpolate = true)
{
if (!IsOwner)
{
throw new Exception("Trying to set a state on a not owned transform");
}
if (m_CachedNetworkManager && !(m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening))
{
return;
}
Vector3 pos = posIn == null ? transform.position : (Vector3)posIn;
Quaternion rot = rotIn == null ? transform.rotation : (Quaternion)rotIn;
Vector3 scale = scaleIn == null ? transform.localScale : (Vector3)scaleIn;
if (!CanCommitToTransform)
{
if (!m_CachedIsServer)
{
SetStateServerRpc(pos, rot, scale, shouldGhostsInterpolate);
}
}
else
{
m_Transform.position = pos;
m_Transform.rotation = rot;
m_Transform.localScale = scale;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldGhostsInterpolate;
}
}
[ServerRpc]
private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport)
{
// server has received this RPC request to move change transform. Give the server a chance to modify or
// even reject the move
if (OnClientRequestChange != null)
{
(pos, rot, scale) = OnClientRequestChange(pos, rot, scale);
}
m_Transform.position = pos;
m_Transform.rotation = rot;
m_Transform.localScale = scale;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport;
}
#endregion
// todo this is currently in update, to be able to catch any transform changes. A FixedUpdate mode could be added to be less intense, but it'd be
// conditional to users only making transform update changes in FixedUpdate.
protected virtual void Update()
{
if (!NetworkObject.IsSpawned)
{
return;
}
if (CanCommitToTransform)
{
if (m_CachedIsServer)
{
TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time);
}
m_PrevNetworkState = m_LocalAuthoritativeNetworkState;
}
// apply interpolated value
if (m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening)
{
// eventually, we could hoist this calculation so that it happens once for all objects, not once per object
var cachedDeltaTime = Time.deltaTime;
var serverTime = NetworkManager.ServerTime;
var cachedServerTime = serverTime.Time;
var cachedRenderTime = serverTime.TimeTicksAgo(1).Time;
foreach (var interpolator in m_AllFloatInterpolators)
{
interpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
}
m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
if (!CanCommitToTransform)
{
#if NGO_TRANSFORM_DEBUG
if (m_CachedNetworkManager.LogLevel == LogLevel.Developer)
{
// TODO: This should be a component gizmo - not some debug draw based on log level
var interpolatedPosition = new Vector3(m_PositionXInterpolator.GetInterpolatedValue(), m_PositionYInterpolator.GetInterpolatedValue(), m_PositionZInterpolator.GetInterpolatedValue());
Debug.DrawLine(interpolatedPosition, interpolatedPosition + Vector3.up, Color.magenta, k_DebugDrawLineTime, false);
// try to update previously consumed NetworkState
// if we have any changes, that means made some updates locally
// we apply the latest ReplNetworkState again to revert our changes
var oldStateDirtyInfo = ApplyTransformToNetworkStateWithInfo(ref m_PrevNetworkState, 0, m_Transform);
// there are several bugs in this code, as we the message is dumped out under odd circumstances
// For Matt, it would trigger when an object's rotation was perturbed by colliding with another
// object vs. explicitly rotating it
if (oldStateDirtyInfo.isPositionDirty || oldStateDirtyInfo.isScaleDirty || (oldStateDirtyInfo.isRotationDirty && SyncRotAngleX && SyncRotAngleY && SyncRotAngleZ))
{
// ignoring rotation dirty since quaternions will mess with euler angles, making this impossible to determine if the change to a single axis comes
// from an unauthorized transform change or euler to quaternion conversion artifacts.
var dirtyField = oldStateDirtyInfo.isPositionDirty ? "position" : oldStateDirtyInfo.isRotationDirty ? "rotation" : "scale";
Debug.LogWarning($"A local change to {dirtyField} without authority detected, reverting back to latest interpolated network state!", this);
}
}
#endif
// Apply updated interpolated value
ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform);
}
}
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
/// <summary>
/// Teleports the transform to the given values without interpolating
/// </summary>
public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newScale)
{
if (!CanCommitToTransform)
{
throw new Exception("Teleport not allowed");
}
var newRotationEuler = newRotation.eulerAngles;
var stateToSend = m_LocalAuthoritativeNetworkState;
stateToSend.IsTeleportingNextFrame = true;
stateToSend.Position = newPosition;
stateToSend.Rotation = newRotationEuler;
stateToSend.Scale = newScale;
ApplyInterpolatedNetworkStateToTransform(stateToSend, transform);
// set teleport flag in state to signal to ghosts not to interpolate
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = true;
// check server side
TryCommitValuesToServer(newPosition, newRotationEuler, newScale, m_CachedNetworkManager.LocalTime.Time);
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e96cb6065543e43c4a752faaa1468eb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
{
"name": "Unity.Netcode.Components",
"rootNamespace": "Unity.Netcode.Components",
"references": [
"Unity.Netcode.Runtime",
"Unity.Collections"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3b8ed52f1b5c64994af4c4e0aa4b6c4b
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: