Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ private void OnEnable()
[SerializeField]
public bool GenerateDefaultNetworkPrefabs = true;

/// <summary>
/// The project wide <see cref="TransformSyncModes"/> that is applied to <see cref="NetworkConfig.TransformSyncMode"/>.
/// </summary>
/// <remarks>
/// The two modes are not wire compatible with one another, so this is authored once for the project as
/// opposed to per <see cref="NetworkManager"/>.
/// </remarks>
[SerializeField]
public TransformSyncModes TransformSyncMode = TransformSyncModes.PerInstance;

internal void SaveSettings()
{
Save(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using Unity.Netcode.Components;
using UnityEditor;
using UnityEngine;
using Directory = UnityEngine.Windows.Directory;
Expand Down Expand Up @@ -132,6 +133,7 @@ private static void OnGuiHandler(string obj)
var settings = NetcodeForGameObjectsProjectSettings.instance;
var generateDefaultPrefabs = settings.GenerateDefaultNetworkPrefabs;
var networkPrefabsPath = settings.TempNetworkPrefabsPath;
var transformSyncMode = settings.TransformSyncMode;

EditorGUI.BeginChangeCheck();

Expand Down Expand Up @@ -192,6 +194,26 @@ private static void OnGuiHandler(string obj)
networkPrefabsPath,
GUILayout.Width(s_MaxLabelWidth + 270));
GUILayout.EndVertical();

GUILayout.BeginVertical("Box");
GUILayout.Label("NetworkTransform Synchronization", EditorStyles.boldLabel);
transformSyncMode = (TransformSyncModes)EditorGUILayout.EnumPopup(
new GUIContent(
"Synchronization Mode",
"Determines how NetworkTransform instances detect and synchronize their state. " +
"Batched mode detects changes for all instances within a job and sends them as a single message per tick. " +
"This is a global setting for all NetworkTransforms since the two modes are not compatible on a per instance basis."),
transformSyncMode,
GUILayout.Width(s_MaxLabelWidth + 120));

if (transformSyncMode == TransformSyncModes.Batched)
{
EditorGUILayout.HelpBox(
$"{nameof(NetworkTransform.UseUnreliableDeltas)} does not apply in this mode and will no longer be visible when viewing " +
"NetworkTransform in the inspector view. Delivery is determined per state update as opposed to per component.",
MessageType.Info);
}
GUILayout.EndVertical();
}
EditorGUILayout.EndFoldoutHeaderGroup();
GUILayout.EndVertical();
Expand All @@ -202,6 +224,7 @@ private static void OnGuiHandler(string obj)
NetcodeForGameObjectsEditorSettings.SetNetcodeInstallMultiplayerToolTips(multiplayerToolsTipStatus ? 0 : 1);
settings.GenerateDefaultNetworkPrefabs = generateDefaultPrefabs;
settings.TempNetworkPrefabsPath = networkPrefabsPath;
settings.TransformSyncMode = transformSyncMode;
settings.SaveSettings();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Unity.Netcode.GameObjects.Editor.Configuration;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Unity.Netcode.Editor
{
/// <summary>
/// Applies the project wide <see cref="NetcodeForGameObjectsProjectSettings.TransformSyncMode"/> to the
/// <see cref="NetworkConfig"/>.
/// </summary>
/// <remarks>
/// This runs both when entering play mode and while building, and operates on the scene being processed as
/// opposed to the authored asset, so it never dirties a user's scene.
/// </remarks>
internal class SetTransformSyncMode : IProcessSceneWithReport
{
public int callbackOrder => 0;

public void OnProcessScene(Scene scene, BuildReport report)
{
var transformSyncMode = NetcodeForGameObjectsProjectSettings.instance.TransformSyncMode;
foreach (var networkManager in FindObjects.FromSceneByType<NetworkManager>(scene, true))
{
if (networkManager.NetworkConfig == null)
{
continue;
}
networkManager.NetworkConfig.TransformSyncMode = transformSyncMode;
}
}
}

/// <summary>
/// Applies the project wide <see cref="NetcodeForGameObjectsProjectSettings.TransformSyncMode"/> to any
/// <see cref="NetworkManager"/> within a prefab as the prefab will be is imported.
/// </summary>
/// <remarks>
/// Covers projects that instantiate their <see cref="NetworkManager"/> from a prefab as opposed to placing
/// it in a scene.
/// </remarks>
internal class TransformSyncModePrefabProcessor : AssetPostprocessor
{
public void OnPostprocessPrefab(GameObject root)
{
var networkManagers = root.GetComponentsInChildren<NetworkManager>(true);
if (networkManagers.Length == 0)
{
return;
}

var transformSyncMode = NetcodeForGameObjectsProjectSettings.instance.TransformSyncMode;
foreach (var networkManager in networkManagers)
{
if (networkManager.NetworkConfig == null)
{
continue;
}
networkManager.NetworkConfig.TransformSyncMode = transformSyncMode;
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 35 additions & 21 deletions com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using Unity.Netcode.Components;
using Unity.Netcode.GameObjects.Editor.Configuration;
using UnityEditor;
using UnityEngine;

Expand Down Expand Up @@ -216,32 +217,42 @@ private void DisplayNetworkTransformProperties()
EditorGUILayout.Space();
EditorGUILayout.LabelField("Delivery", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_TickSyncChildren);
// If both are set from a previous configuration, then SwitchTransformSpaceWhenParented takes
// precedence.
if (networkTransform.UseUnreliableDeltas && networkTransform.SwitchTransformSpaceWhenParented)
{
networkTransform.UseUnreliableDeltas = false;
}
SetGUIActive(!networkTransform.SwitchTransformSpaceWhenParented);
if (networkTransform.SwitchTransformSpaceWhenParented)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(m_UseUnreliableDeltas);
EditorGUILayout.LabelField($"Cannot use with {nameof(NetworkTransform.SwitchTransformSpaceWhenParented)}.");
EditorGUILayout.EndHorizontal();
}
else

// UseUnreliableDeltas only applies to per instance synchronization mode. Under the batched mode
// delivery is determined per state update as opposed to per component, so the property (and
// everything it constrains) is hidden. See Project Settings -> Multiplayer -> Netcode for GameObjects.
var perInstanceSync = NetcodeForGameObjectsProjectSettings.instance.TransformSyncMode == TransformSyncModes.PerInstance;
if (perInstanceSync)
{
EditorGUILayout.PropertyField(m_UseUnreliableDeltas);
}
// If both are set from a previous configuration, then SwitchTransformSpaceWhenParented takes
// precedence.
if (networkTransform.UseUnreliableDeltas && networkTransform.SwitchTransformSpaceWhenParented)
{
networkTransform.UseUnreliableDeltas = false;
}
SetGUIActive(!networkTransform.SwitchTransformSpaceWhenParented);
if (networkTransform.SwitchTransformSpaceWhenParented)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(m_UseUnreliableDeltas);
EditorGUILayout.LabelField($"Cannot use with {nameof(NetworkTransform.SwitchTransformSpaceWhenParented)}.");
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.PropertyField(m_UseUnreliableDeltas);
}

SetGUIActive(true);
SetGUIActive(true);
}

EditorGUILayout.Space();
EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel);

SetGUIActive(!networkTransform.UseUnreliableDeltas);
if (networkTransform.UseUnreliableDeltas)
// SwitchTransformSpaceWhenParented is only constrained by UseUnreliableDeltas while the latter applies.
var blockedByUnreliableDeltas = perInstanceSync && networkTransform.UseUnreliableDeltas;
SetGUIActive(!blockedByUnreliableDeltas);
if (blockedByUnreliableDeltas)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(m_SwitchTransformSpaceWhenParented);
Expand All @@ -256,7 +267,10 @@ private void DisplayNetworkTransformProperties()
if (m_SwitchTransformSpaceWhenParented.boolValue)
{
m_TickSyncChildren.boolValue = true;
networkTransform.UseUnreliableDeltas = false;
if (perInstanceSync)
{
networkTransform.UseUnreliableDeltas = false;
}
}
else
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Unity.Burst;
using Unity.Collections;
using UnityEngine.Jobs;
using static Unity.Netcode.Components.NetworkTransform;

namespace Unity.Netcode.Components
{
/// <summary>
/// Motion Authority Only:
/// Detects <see cref="NetworkTransform"/> state changes for every registered instance in parallel.
/// </summary>
/// <remarks>
/// This reads each transform and defers to the very same
/// <see cref="CheckForStateChange(ref NetworkTransformState, ref NetworkDeltaPosition, ref TransformDeltaConfig, in TransformSample, bool, bool, bool)"/>
/// that the per instance path runs on the main thread to keep the logic between per instance and batched the same.<br />
/// Only transform values are read here and only the entries array is written, so there is no hierarchy
/// write hazard: nothing in this job touches a transform other than the one at its own index, and nothing
/// writes to a transform at all.
/// </remarks>
[BurstCompile]
internal struct DetectTransformDeltaJob : IJobParallelForTransform
{
/// <summary>
/// The per instance input and output, parallel to the transforms this job is scheduled over.
/// </summary>
public NativeArray<TransformDeltaEntry> Entries;

public void Execute(int index, TransformAccess transform)
{
if (!transform.isValid)
{
return;
}

var entry = Entries[index];
var flagStates = entry.State.FlagStates;
var forceState = entry.ForceState;

// Resolve the transform space before sampling, otherwise the wrong set of values gets compared.
var transformSpaceChanged = ResolveTransformSpace(ref entry.Config, ref flagStates, entry.TransformHasParent, false, ref forceState);
entry.State.FlagStates = flagStates;

// A rigidbody driven instance cannot be sampled from here, so it is never registered for the
// batched path and always falls back to the per instance flow.
var rotation = entry.Config.InLocalSpace ? transform.localRotation : transform.rotation;
entry.Sample.Position = entry.Config.InLocalSpace ? transform.localPosition : transform.position;
entry.Sample.Rotation = rotation;
entry.Sample.RotAngles = NetworkTransformMath.EulerAngles(rotation);
entry.Sample.Scale = transform.localScale;

entry.IsDirty = CheckForStateChange(ref entry.State, ref entry.HalfPositionState, ref entry.Config,
entry.Sample, false, forceState, transformSpaceChanged);

Entries[index] = entry;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 30 additions & 18 deletions com.unity.netcode.gameobjects/Runtime/Components/HalfVector3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,19 @@ public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReade
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3()
{
Vector3 fullPrecision = Vector3.zero;
Vector3 fullConversion = math.float3(Axis);
for (int i = 0; i < Length; i++)
{
if (AxisToSynchronize[i])
{
fullPrecision[i] = fullConversion[i];
}
}
return fullPrecision;
return ToFloat3(Axis, AxisToSynchronize);
}

/// <summary>
/// The <see cref="float3"/> based implementation of <see cref="ToVector3"/>.
/// </summary>
/// <remarks>
/// This is a job safe method to be used in place of <see cref="ToVector3"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static float3 ToFloat3(half3 axis, bool3 axisToSynchronize)
{
return math.select(float3.zero, math.float3(axis), axisToSynchronize);
}

/// <summary>
Expand All @@ -115,14 +118,23 @@ public Vector3 ToVector3()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateFrom(ref Vector3 vector3)
{
var half3Full = math.half3(vector3);
for (int i = 0; i < Length; i++)
{
if (AxisToSynchronize[i])
{
Axis[i] = half3Full[i];
}
}
Axis = UpdatedAxis(Axis, math.float3(vector3), AxisToSynchronize);
}

/// <summary>
/// The <see cref="half3"/> based implementation of <see cref="UpdateFrom(ref Vector3)"/>.
/// </summary>
/// <remarks>
/// This is a job safe method to be used in place of <see cref="UpdateFrom(ref Vector3)"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static half3 UpdatedAxis(half3 axis, float3 value, bool3 axisToSynchronize)
{
var updated = math.half3(value);
axis.x = axisToSynchronize.x ? updated.x : axis.x;
axis.y = axisToSynchronize.y ? updated.y : axis.y;
axis.z = axisToSynchronize.z ? updated.z : axis.z;
return axis;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,24 @@ internal void ResetTo(Transform parent, T targetValue, double serverTime)
{
// Clear the interpolator
Clear();
InternalReset(parent, targetValue, serverTime);

// The baseline measurement is deliberately not seeded here. Callers stamp it with
// NetworkManager.ServerTime.Time (the local current time) while the measurements that follow are
// stamped with the tick they were authored on (NetworkTransformState.SentTime), which is always at
// least a tick older. Seeding the baseline therefore establishes an ordering floor that later
// measurements cannot clear: AddMeasurement drops anything not newer than m_LastMeasurementAddedTime,
// and TryConsumeFromBuffer drops anything not newer than InterpolateState.Target.TimeSent.
//
// This only reaches an instance that resets part way through a session, which in practice means one
// that just stopped being the authority (in a client server topology, only ever the server). Such an
// instance would otherwise reject everything the new authority sends until a measurement happens to
// be authored on a later tick than the reset, and if motion has already stopped that never arrives.
//
// Clear() has left the buffer empty with a zeroed m_LastMeasurementAddedTime, and InternalReset seeds
// CurrentValue/NextValue/PreviousValue below, so the value is still held. That is exactly the state a
// freshly spawned interpolator is in: the first measurement to arrive is taken unconditionally
// because m_BufferCount is zero, and it is consumed against render time alone.
InternalReset(parent, targetValue, serverTime, false);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Loading