com.unity.netcode.gameobjects@2.0.0-exp.4
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). ## [2.0.0-exp.4] - 2024-05-31 ### Added - Added `NetworkRigidbodyBase.AttachToFixedJoint` and `NetworkRigidbodyBase.DetachFromFixedJoint` to replace parenting for rigid bodies that have `NetworkRigidbodyBase.UseRigidBodyForMotion` enabled. (#2933) - Added `NetworkBehaviour.OnNetworkPreSpawn` and `NetworkBehaviour.OnNetworkPostSpawn` methods that provide the ability to handle pre and post spawning actions during the `NetworkObject` spawn sequence. (#2912) - Added a client-side only `NetworkBehaviour.OnNetworkSessionSynchronized` convenience method that is invoked on all `NetworkBehaviour`s after a newly joined client has finished synchronizing with the network session in progress. (#2912) - Added `NetworkBehaviour.OnInSceneObjectsSpawned` convenience method that is invoked when all in-scene `NetworkObject`s have been spawned after a scene has been loaded or upon a host or server starting. (#2912) ### Fixed - Fixed issue where non-authoritative rigid bodies with `NetworkRigidbodyBase.UseRigidBodyForMotion` enabled would constantly log errors about the renderTime being before `StartTimeConsumed`. (#2933) - Fixed issue where in-scene placed NetworkObjects could be destroyed if a client disconnects early and/or before approval. (#2924) - Fixed issue where a `NetworkObject` component's associated `NetworkBehaviour` components would not be detected if scene loading is disabled in the editor and the currently loaded scene has in-scene placed `NetworkObject`s. (#2912) - Fixed issue where an in-scene placed `NetworkObject` with `NetworkTransform` that is also parented under a `GameObject` would not properly synchronize when the parent `GameObject` had a world space position other than 0,0,0. (#2898) ### Changed - Change all the access modifiers of test class from Public to Internal (#2930) - Changed messages are now sorted by enum values as opposed to ordinally sorting the messages by their type name. (#2929) - Changed `NetworkClient.SessionModeTypes` to `NetworkClient.NetworkTopologyTypes`. (#2875) - Changed `NetworkClient.SessionModeType` to `NetworkClient.NetworkTopologyType`. (#2875) - Changed `NetworkConfig.SessionMode` to `NeworkConfig.NetworkTopology`. (#2875)
This commit is contained in:
170
Runtime/Components/HalfVector3.cs
Normal file
170
Runtime/Components/HalfVector3.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Netcode.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Half float precision <see cref="Vector3"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Vector3T<ushort> values are half float values returned by <see cref="Mathf.FloatToHalf(float)"/> for each
|
||||
/// individual axis and the 16 bits of the half float are stored as <see cref="ushort"/> values since C# does not have
|
||||
/// a half float type.
|
||||
/// </remarks>
|
||||
public struct HalfVector3 : INetworkSerializable
|
||||
{
|
||||
internal const int Length = 3;
|
||||
|
||||
/// <summary>
|
||||
/// The half float precision value of the x-axis as a <see cref="half"/>.
|
||||
/// </summary>
|
||||
public half X => Axis.x;
|
||||
/// <summary>
|
||||
/// The half float precision value of the y-axis as a <see cref="half"/>.
|
||||
/// </summary>
|
||||
public half Y => Axis.y;
|
||||
/// <summary>
|
||||
/// The half float precision value of the z-axis as a <see cref="half"/>.
|
||||
/// </summary>
|
||||
public half Z => Axis.z;
|
||||
|
||||
/// <summary>
|
||||
/// Used to store the half float precision values as a <see cref="half3"/>
|
||||
/// </summary>
|
||||
public half3 Axis;
|
||||
|
||||
/// <summary>
|
||||
/// Determine which axis will be synchronized during serialization
|
||||
/// </summary>
|
||||
public bool3 AxisToSynchronize;
|
||||
|
||||
/// <summary>
|
||||
/// Directly sets each axial value to the passed in full precision values
|
||||
/// that are converted to half precision
|
||||
/// </summary>
|
||||
internal void Set(float x, float y, float z)
|
||||
{
|
||||
Axis.x = math.half(x);
|
||||
Axis.y = math.half(y);
|
||||
Axis.z = math.half(z);
|
||||
}
|
||||
|
||||
private void SerializeWrite(FastBufferWriter writer)
|
||||
{
|
||||
for (int i = 0; i < Length; i++)
|
||||
{
|
||||
if (AxisToSynchronize[i])
|
||||
{
|
||||
writer.WriteUnmanagedSafe(Axis[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SerializeRead(FastBufferReader reader)
|
||||
{
|
||||
for (int i = 0; i < Length; i++)
|
||||
{
|
||||
if (AxisToSynchronize[i])
|
||||
{
|
||||
var axisValue = Axis[i];
|
||||
reader.ReadUnmanagedSafe(out axisValue);
|
||||
Axis[i] = axisValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The serialization implementation of <see cref="INetworkSerializable"/>.
|
||||
/// </summary>
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||
{
|
||||
if (serializer.IsReader)
|
||||
{
|
||||
SerializeRead(serializer.GetFastBufferReader());
|
||||
}
|
||||
else
|
||||
{
|
||||
SerializeWrite(serializer.GetFastBufferWriter());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full precision value as a <see cref="Vector3"/>.
|
||||
/// </summary>
|
||||
/// <returns>a <see cref="Vector3"/> as the full precision value.</returns>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a full precision <see cref="Vector3"/> to half precision and updates the current instance.
|
||||
/// </summary>
|
||||
/// <param name="vector3">The <see cref="Vector3"/> to convert.</param>
|
||||
[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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
|
||||
/// <param name="vector3AxisToSynchronize">The axis to synchronize.</param>
|
||||
public HalfVector3(Vector3 vector3, bool3 axisToSynchronize)
|
||||
{
|
||||
Axis = half3.zero;
|
||||
AxisToSynchronize = axisToSynchronize;
|
||||
UpdateFrom(ref vector3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor that defaults to all axis being synchronized.
|
||||
/// </summary>
|
||||
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
|
||||
public HalfVector3(Vector3 vector3) : this(vector3, math.bool3(true))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="axisToSynchronize">The axis to synchronize.</param>
|
||||
public HalfVector3(float x, float y, float z, bool3 axisToSynchronize) : this(new Vector3(x, y, z), axisToSynchronize)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor that defaults to all axis being synchronized.
|
||||
/// </summary>
|
||||
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
|
||||
public HalfVector3(float x, float y, float z) : this(new Vector3(x, y, z), math.bool3(true))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Components/HalfVector3.cs.meta
Normal file
11
Runtime/Components/HalfVector3.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0e371533eaeac446b16b10886f64f84
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
137
Runtime/Components/HalfVector4.cs
Normal file
137
Runtime/Components/HalfVector4.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Netcode.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Half Precision <see cref="Vector4"/> that can also be used to convert a <see cref="Quaternion"/> to half precision.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Vector4T<ushort> values are half float values returned by <see cref="Mathf.FloatToHalf(float)"/> for each
|
||||
/// individual axis and the 16 bits of the half float are stored as <see cref="ushort"/> values since C# does not have
|
||||
/// a half float type.
|
||||
/// </remarks>
|
||||
public struct HalfVector4 : INetworkSerializable
|
||||
{
|
||||
internal const int Length = 4;
|
||||
/// <summary>
|
||||
/// The half float precision value of the x-axis as a <see cref="half"/>.
|
||||
/// </summary>
|
||||
public half X => Axis.x;
|
||||
|
||||
/// <summary>
|
||||
/// The half float precision value of the y-axis as a <see cref="half"/>.
|
||||
/// </summary>
|
||||
public half Y => Axis.y;
|
||||
|
||||
/// <summary>
|
||||
/// The half float precision value of the z-axis as a <see cref="half"/>.
|
||||
/// </summary>
|
||||
public half Z => Axis.z;
|
||||
|
||||
/// <summary>
|
||||
/// The half float precision value of the w-axis as a <see cref="half"/>.
|
||||
/// </summary>
|
||||
public half W => Axis.w;
|
||||
|
||||
/// <summary>
|
||||
/// Used to store the half float precision values as a <see cref="half4"/>
|
||||
/// </summary>
|
||||
public half4 Axis;
|
||||
|
||||
private void SerializeWrite(FastBufferWriter writer)
|
||||
{
|
||||
for (int i = 0; i < Length; i++)
|
||||
{
|
||||
writer.WriteUnmanagedSafe(Axis[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void SerializeRead(FastBufferReader reader)
|
||||
{
|
||||
for (int i = 0; i < Length; i++)
|
||||
{
|
||||
var axisValue = Axis[i];
|
||||
reader.ReadUnmanagedSafe(out axisValue);
|
||||
Axis[i] = axisValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The serialization implementation of <see cref="INetworkSerializable"/>.
|
||||
/// </summary>
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||
{
|
||||
if (serializer.IsReader)
|
||||
{
|
||||
SerializeRead(serializer.GetFastBufferReader());
|
||||
}
|
||||
else
|
||||
{
|
||||
SerializeWrite(serializer.GetFastBufferWriter());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this instance to a full precision <see cref="Vector4"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Vector4"/> as the full precision value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector4 ToVector4()
|
||||
{
|
||||
return math.float4(Axis);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this instance to a full precision <see cref="Quaternion"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Quaternion"/> as the full precision value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Quaternion ToQuaternion()
|
||||
{
|
||||
return math.quaternion(Axis);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a full precision <see cref="Vector4"/> to half precision and updates the current instance.
|
||||
/// </summary>
|
||||
/// <param name="vector4">The <see cref="Vector4"/> to convert and update this instance with.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void UpdateFrom(ref Vector4 vector4)
|
||||
{
|
||||
Axis = math.half4(vector4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a full precision <see cref="Vector4"/> to half precision and updates the current instance.
|
||||
/// </summary>
|
||||
/// <param name="quaternion">The <see cref="Quaternion"/> to convert and update this instance with.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void UpdateFrom(ref Quaternion quaternion)
|
||||
{
|
||||
Axis = math.half4(math.half(quaternion.x), math.half(quaternion.y), math.half(quaternion.z), math.half(quaternion.w));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="vector4">The initial axial values (converted to half floats) when instantiated.</param>
|
||||
public HalfVector4(Vector4 vector4)
|
||||
{
|
||||
Axis = default;
|
||||
UpdateFrom(ref vector4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="w">The initial w axis (converted to half float) value when instantiated.</param>
|
||||
public HalfVector4(float x, float y, float z, float w) : this(new Vector4(x, y, z, w))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Components/HalfVector4.cs.meta
Normal file
11
Runtime/Components/HalfVector4.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03c78136f41ff84499e2a6ac4a7dd7a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Runtime/Components/Interpolator.meta
Normal file
8
Runtime/Components/Interpolator.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8eb56856ab05d41fa9e422a92acbc109
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
392
Runtime/Components/Interpolator/BufferedLinearInterpolator.cs
Normal file
392
Runtime/Components/Interpolator/BufferedLinearInterpolator.cs
Normal file
@@ -0,0 +1,392 @@
|
||||
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">The type of interpolated value</typeparam>
|
||||
public abstract class BufferedLinearInterpolator<T> where T : struct
|
||||
{
|
||||
internal float MaxInterpolationBound = 3.0f;
|
||||
private struct BufferedItem
|
||||
{
|
||||
public T Item;
|
||||
public double TimeSent;
|
||||
|
||||
public BufferedItem(T item, double timeSent)
|
||||
{
|
||||
Item = item;
|
||||
TimeSent = timeSent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// There's two factors affecting interpolation: buffering (set in NetworkManager's NetworkTimeSystem) and interpolation time, which is the amount of time it'll take to reach the target. This is to affect the second one.
|
||||
/// </summary>
|
||||
public float MaximumInterpolationTime = 0.1f;
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Resets interpolator to initial state
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
m_Buffer.Clear();
|
||||
m_EndTimeConsumed = 0.0d;
|
||||
m_StartTimeConsumed = 0.0d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Teleports current interpolation value to targetValue.
|
||||
/// </summary>
|
||||
/// <param name="targetValue">The target value to teleport instantly</param>
|
||||
/// <param name="serverTime">The current server time</param>
|
||||
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>
|
||||
/// <returns>The newly interpolated value of type 'T'</returns>
|
||||
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>
|
||||
/// <returns>The newly interpolated value of type 'T'</returns>
|
||||
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)
|
||||
{
|
||||
var rangeFactor = 1.0f / (float)range;
|
||||
|
||||
t = ((float)renderTime - (float)m_StartTimeConsumed) * rangeFactor;
|
||||
|
||||
if (t < 0.0f)
|
||||
{
|
||||
// There is no mechanism to guarantee renderTime to not be before m_StartTimeConsumed
|
||||
// This clamps t to a minimum of 0 and fixes issues with longer frames and pauses
|
||||
|
||||
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
|
||||
{
|
||||
NetworkLog.LogError($"renderTime was before m_StartTimeConsumed. This should never happen. {nameof(renderTime)} is {renderTime}, {nameof(m_StartTimeConsumed)} is {m_StartTimeConsumed}");
|
||||
}
|
||||
t = 0.0f;
|
||||
}
|
||||
|
||||
if (t > MaxInterpolationBound) // max extrapolation
|
||||
{
|
||||
// TODO this causes issues with teleport, investigate
|
||||
t = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
var target = InterpolateUnclamped(m_InterpStartValue, m_InterpEndValue, t);
|
||||
m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / MaximumInterpolationTime); // second interpolate to smooth out extrapolation jumps
|
||||
}
|
||||
|
||||
m_NbItemsReceivedThisFrame = 0;
|
||||
return m_CurrentInterpValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add measurements to be used during interpolation. These will be buffered before being made available to be displayed as "latest value".
|
||||
/// </summary>
|
||||
/// <param name="newMeasurement">The new measurement value to use</param>
|
||||
/// <param name="sentTime">The time to record for measurement</param>
|
||||
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);
|
||||
// Next line keeps renderTime above m_StartTimeConsumed. Fixes pause/unpause issues
|
||||
m_Buffer.Add(m_LastBufferedItemReceived);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Part the of reason for disabling extrapolation is how we add and use measurements over time.
|
||||
// TODO: Add detailed description of this area in Jira ticket
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets latest value from the interpolator. This is updated every update as time goes by.
|
||||
/// </summary>
|
||||
/// <returns>The current interpolated value of type 'T'</returns>
|
||||
public T GetInterpolatedValue()
|
||||
{
|
||||
return m_CurrentInterpValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to override and adapted to the generic type. This assumes interpolation for that value will be clamped.
|
||||
/// </summary>
|
||||
/// <param name="start">The start value (min)</param>
|
||||
/// <param name="end">The end value (max)</param>
|
||||
/// <param name="time">The time value used to interpolate between start and end values (pos)</param>
|
||||
/// <returns>The interpolated value</returns>
|
||||
protected abstract T Interpolate(T start, T end, float time);
|
||||
|
||||
/// <summary>
|
||||
/// Method to override and adapted to the generic type. This assumes interpolation for that value will not be clamped.
|
||||
/// </summary>
|
||||
/// <param name="start">The start value (min)</param>
|
||||
/// <param name="end">The end value (max)</param>
|
||||
/// <param name="time">The time value used to interpolate between start and end values (pos)</param>
|
||||
/// <returns>The interpolated value</returns>
|
||||
protected abstract T InterpolateUnclamped(T start, T end, float time);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// This is a buffered linear interpolator for a <see cref="float"/> type value
|
||||
/// </remarks>
|
||||
public class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator<float>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override float InterpolateUnclamped(float start, float end, float time)
|
||||
{
|
||||
// Disabling Extrapolation:
|
||||
// TODO: Add Jira Ticket
|
||||
return Mathf.Lerp(start, end, time);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float Interpolate(float start, float end, float time)
|
||||
{
|
||||
return Mathf.Lerp(start, end, time);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// This is a buffered linear interpolator for a <see cref="Quaternion"/> type value
|
||||
/// </remarks>
|
||||
public class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator<Quaternion>
|
||||
{
|
||||
/// <summary>
|
||||
/// Use <see cref="Quaternion.Slerp"/> when <see cref="true"/>.
|
||||
/// Use <see cref="Quaternion.Lerp"/> when <see cref="false"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When using half precision (due to the imprecision) using <see cref="Quaternion.Lerp"/> is
|
||||
/// less processor intensive (i.e. precision is already "imprecise").
|
||||
/// When using full precision (to maintain precision) using <see cref="Quaternion.Slerp"/> is
|
||||
/// more processor intensive yet yields more precise results.
|
||||
/// </remarks>
|
||||
public bool IsSlerp;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
|
||||
{
|
||||
if (IsSlerp)
|
||||
{
|
||||
return Quaternion.Slerp(start, end, time);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Quaternion.Lerp(start, end, time);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
|
||||
{
|
||||
if (IsSlerp)
|
||||
{
|
||||
return Quaternion.Slerp(start, end, time);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Quaternion.Lerp(start, end, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BufferedLinearInterpolator<T>"/> <see cref="Vector3"/> implementation.
|
||||
/// </summary>
|
||||
public class BufferedLinearInterpolatorVector3 : BufferedLinearInterpolator<Vector3>
|
||||
{
|
||||
/// <summary>
|
||||
/// Use <see cref="Vector3.Slerp"/> when <see cref="true"/>.
|
||||
/// Use <see cref="Vector3.Lerp"/> when <see cref="false"/>
|
||||
/// </summary>
|
||||
public bool IsSlerp;
|
||||
/// <inheritdoc />
|
||||
protected override Vector3 InterpolateUnclamped(Vector3 start, Vector3 end, float time)
|
||||
{
|
||||
if (IsSlerp)
|
||||
{
|
||||
return Vector3.Slerp(start, end, time);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Vector3.Lerp(start, end, time);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector3 Interpolate(Vector3 start, Vector3 end, float time)
|
||||
{
|
||||
if (IsSlerp)
|
||||
{
|
||||
return Vector3.Slerp(start, end, time);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Vector3.Lerp(start, end, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a12ebf95bdb4445d9a16e4b6adadb6aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Runtime/Components/Messages.meta
Normal file
8
Runtime/Components/Messages.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9db1d18fa0117f4da5e8e65386b894a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1498
Runtime/Components/NetworkAnimator.cs
Normal file
1498
Runtime/Components/NetworkAnimator.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Runtime/Components/NetworkAnimator.cs.meta
Normal file
11
Runtime/Components/NetworkAnimator.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8d0727d5ae3244e3b569694d3912374
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
221
Runtime/Components/NetworkDeltaPosition.cs
Normal file
221
Runtime/Components/NetworkDeltaPosition.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Netcode.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to synchromnize delta position when half float precision is enabled
|
||||
/// </summary>
|
||||
public struct NetworkDeltaPosition : INetworkSerializable
|
||||
{
|
||||
internal const float MaxDeltaBeforeAdjustment = 64f;
|
||||
|
||||
/// <summary>
|
||||
/// The HalfVector3 used to synchronize the delta in position
|
||||
/// </summary>
|
||||
public HalfVector3 HalfVector3;
|
||||
|
||||
internal Vector3 CurrentBasePosition;
|
||||
internal Vector3 PrecisionLossDelta;
|
||||
internal Vector3 HalfDeltaConvertedBack;
|
||||
internal Vector3 PreviousPosition;
|
||||
internal Vector3 DeltaPosition;
|
||||
internal int NetworkTick;
|
||||
|
||||
internal bool SynchronizeBase;
|
||||
|
||||
internal bool CollapsedDeltaIntoBase;
|
||||
|
||||
/// <summary>
|
||||
/// The serialization implementation of <see cref="INetworkSerializable"/>
|
||||
/// </summary>
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||
{
|
||||
if (!SynchronizeBase)
|
||||
{
|
||||
HalfVector3.NetworkSerialize(serializer);
|
||||
}
|
||||
else
|
||||
{
|
||||
serializer.SerializeValue(ref DeltaPosition);
|
||||
serializer.SerializeValue(ref CurrentBasePosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full precision value of Vector3 position while also potentially updating the current base position.
|
||||
/// </summary>
|
||||
/// <param name="networkTick">Use the current network tick value.</param>
|
||||
/// <returns>The full position as a <see cref="Vector3"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector3 ToVector3(int networkTick)
|
||||
{
|
||||
// When synchronizing, it is possible to have a state update arrive
|
||||
// for the same synchronization network tick. Under this scenario,
|
||||
// we only want to return the existing CurrentBasePosition + DeltaPosition
|
||||
// values and not process the X, Y, or Z values.
|
||||
// (See the constructors below)
|
||||
if (networkTick == NetworkTick)
|
||||
{
|
||||
return CurrentBasePosition + DeltaPosition;
|
||||
}
|
||||
for (int i = 0; i < HalfVector3.Length; i++)
|
||||
{
|
||||
if (HalfVector3.AxisToSynchronize[i])
|
||||
{
|
||||
DeltaPosition[i] = Mathf.HalfToFloat(HalfVector3.Axis[i].value);
|
||||
// If we exceed or are equal to the maximum delta value then we need to
|
||||
// apply the delta to the CurrentBasePosition value and reset the delta
|
||||
// position for the axis.
|
||||
if (Mathf.Abs(DeltaPosition[i]) >= MaxDeltaBeforeAdjustment)
|
||||
{
|
||||
CurrentBasePosition[i] += DeltaPosition[i];
|
||||
DeltaPosition[i] = 0.0f;
|
||||
HalfVector3.Axis[i] = half.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
return CurrentBasePosition + DeltaPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current base position (excluding the delta position offset).
|
||||
/// </summary>
|
||||
/// <returns>The current base position as a <see cref="Vector3"/></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector3 GetCurrentBasePosition()
|
||||
{
|
||||
return CurrentBasePosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the full position which includes the delta offset position.
|
||||
/// </summary>
|
||||
/// <returns>The full position as a <see cref="Vector3"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector3 GetFullPosition()
|
||||
{
|
||||
return CurrentBasePosition + DeltaPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The half float vector3 version of the current delta position.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only applies to the authoritative side for <see cref="NetworkTransform"/> instances.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector3 GetConvertedDelta()
|
||||
{
|
||||
return HalfDeltaConvertedBack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The full precision current delta position.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Authoritative: Will have no precision loss
|
||||
/// Non-Authoritative: Has the current network tick's loss of precision.
|
||||
/// Precision loss adjustments are one network tick behind on the
|
||||
/// non-authoritative side.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector3 GetDeltaPosition()
|
||||
{
|
||||
return DeltaPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the position delta based off of the current base position.
|
||||
/// </summary>
|
||||
/// <param name="vector3">The full precision <see cref="Vector3"/> value to (converted to half floats) used to determine the delta offset positon.</param>
|
||||
/// <param name="networkTick">Set the current network tick value when updating.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void UpdateFrom(ref Vector3 vector3, int networkTick)
|
||||
{
|
||||
CollapsedDeltaIntoBase = false;
|
||||
NetworkTick = networkTick;
|
||||
DeltaPosition = (vector3 + PrecisionLossDelta) - CurrentBasePosition;
|
||||
for (int i = 0; i < HalfVector3.Length; i++)
|
||||
{
|
||||
if (HalfVector3.AxisToSynchronize[i])
|
||||
{
|
||||
HalfVector3.Axis[i] = math.half(DeltaPosition[i]);
|
||||
HalfDeltaConvertedBack[i] = Mathf.HalfToFloat(HalfVector3.Axis[i].value);
|
||||
PrecisionLossDelta[i] = DeltaPosition[i] - HalfDeltaConvertedBack[i];
|
||||
if (Mathf.Abs(HalfDeltaConvertedBack[i]) >= MaxDeltaBeforeAdjustment)
|
||||
{
|
||||
CurrentBasePosition[i] += HalfDeltaConvertedBack[i];
|
||||
HalfDeltaConvertedBack[i] = 0.0f;
|
||||
DeltaPosition[i] = 0.0f;
|
||||
CollapsedDeltaIntoBase = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < HalfVector3.Length; i++)
|
||||
{
|
||||
if (HalfVector3.AxisToSynchronize[i])
|
||||
{
|
||||
PreviousPosition[i] = vector3[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
|
||||
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
|
||||
/// <param name="axisToSynchronize">The axis to be synchronized.</param>
|
||||
public NetworkDeltaPosition(Vector3 vector3, int networkTick, bool3 axisToSynchronize)
|
||||
{
|
||||
NetworkTick = networkTick;
|
||||
CurrentBasePosition = vector3;
|
||||
PreviousPosition = vector3;
|
||||
PrecisionLossDelta = Vector3.zero;
|
||||
DeltaPosition = Vector3.zero;
|
||||
HalfDeltaConvertedBack = Vector3.zero;
|
||||
HalfVector3 = new HalfVector3(vector3, axisToSynchronize);
|
||||
SynchronizeBase = false;
|
||||
CollapsedDeltaIntoBase = false;
|
||||
UpdateFrom(ref vector3, networkTick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor that defaults to all axis being synchronized.
|
||||
/// </summary>
|
||||
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
|
||||
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
|
||||
public NetworkDeltaPosition(Vector3 vector3, int networkTick) : this(vector3, networkTick, math.bool3(true))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
|
||||
/// <param name="axisToSynchronize">The axis to be synchronized.</param>
|
||||
public NetworkDeltaPosition(float x, float y, float z, int networkTick, bool3 axisToSynchronize) :
|
||||
this(new Vector3(x, y, z), networkTick, axisToSynchronize)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
|
||||
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
|
||||
public NetworkDeltaPosition(float x, float y, float z, int networkTick) :
|
||||
this(new Vector3(x, y, z), networkTick, math.bool3(true))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Components/NetworkDeltaPosition.cs.meta
Normal file
11
Runtime/Components/NetworkDeltaPosition.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e45e6886578116f4c92fa0fe0d77fb85
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
722
Runtime/Components/NetworkRigidBodyBase.cs
Normal file
722
Runtime/Components/NetworkRigidBodyBase.cs
Normal file
@@ -0,0 +1,722 @@
|
||||
#if COM_UNITY_MODULES_PHYSICS
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Netcode.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// NetworkRigidbodyBase is a unified <see cref="Rigidbody"/> and <see cref="Rigidbody2D"/> integration that helps to synchronize physics motion, collision, and interpolation
|
||||
/// when used with a <see cref="NetworkTransform"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a customizable netcode Rigidbody, create your own component from this class and use <see cref="Initialize(RigidbodyTypes, NetworkTransform, Rigidbody2D, Rigidbody)"/>
|
||||
/// during instantiation (i.e. invoked from within the Awake method). You can re-initialize after having initialized but only when the <see cref="NetworkObject"/> is not spawned.
|
||||
/// </remarks>
|
||||
public abstract class NetworkRigidbodyBase : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// When enabled, the associated <see cref="NetworkTransform"/> will use the Rigidbody/Rigidbody2D to apply and synchronize changes in position, rotation, and
|
||||
/// allows for the use of Rigidbody interpolation/extrapolation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="NetworkTransform.Interpolate"/> is enabled, non-authoritative instances can only use Rigidbody interpolation. If a network prefab is set to
|
||||
/// extrapolation and <see cref="NetworkTransform.Interpolate"/> is enabled, then non-authoritative instances will automatically be adjusted to use Rigidbody
|
||||
/// interpolation while the authoritative instance will still use extrapolation.
|
||||
/// </remarks>
|
||||
[Tooltip("When enabled and a NetworkTransform component is attached, the NetworkTransform will use the rigid body for motion and detecting changes in state.")]
|
||||
public bool UseRigidBodyForMotion;
|
||||
|
||||
/// <summary>
|
||||
/// When enabled (default), automatically set the Kinematic state of the Rigidbody based on ownership.
|
||||
/// When disabled, Kinematic state needs to be set by external script(s).
|
||||
/// </summary>
|
||||
public bool AutoUpdateKinematicState = true;
|
||||
|
||||
/// <summary>
|
||||
/// Primarily applies to the <see cref="AutoUpdateKinematicState"/> property when disabled but you still want
|
||||
/// the Rigidbody to be automatically set to Kinematic when despawned.
|
||||
/// </summary>
|
||||
public bool AutoSetKinematicOnDespawn = true;
|
||||
|
||||
// Determines if this is a Rigidbody or Rigidbody2D implementation
|
||||
private bool m_IsRigidbody2D => RigidbodyType == RigidbodyTypes.Rigidbody2D;
|
||||
// Used to cache the authority state of this Rigidbody during the last frame
|
||||
private bool m_IsAuthority;
|
||||
private Rigidbody m_Rigidbody;
|
||||
private Rigidbody2D m_Rigidbody2D;
|
||||
internal NetworkTransform NetworkTransform;
|
||||
private enum InterpolationTypes
|
||||
{
|
||||
None,
|
||||
Interpolate,
|
||||
Extrapolate
|
||||
}
|
||||
private InterpolationTypes m_OriginalInterpolation;
|
||||
|
||||
/// <summary>
|
||||
/// Used to define the type of Rigidbody implemented.
|
||||
/// <see cref=""/>
|
||||
/// </summary>
|
||||
public enum RigidbodyTypes
|
||||
{
|
||||
Rigidbody,
|
||||
Rigidbody2D,
|
||||
}
|
||||
|
||||
public RigidbodyTypes RigidbodyType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the networked Rigidbody based on the <see cref="RigidbodyTypes"/>
|
||||
/// passed in as a parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Cannot be initialized while the associated <see cref="NetworkObject"/> is spawned.
|
||||
/// </remarks>
|
||||
/// <param name="rigidbodyType">type of rigid body being initialized</param>
|
||||
/// <param name="rigidbody2D">(optional) The <see cref="Rigidbody2D"/> to be used</param>
|
||||
/// <param name="rigidbody">(optional) The <see cref="Rigidbody"/> to be used</param>
|
||||
protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform networkTransform = null, Rigidbody2D rigidbody2D = null, Rigidbody rigidbody = null)
|
||||
{
|
||||
// Don't initialize if already spawned
|
||||
if (IsSpawned)
|
||||
{
|
||||
Debug.LogError($"[{name}] Attempting to initialize while spawned is not allowed.");
|
||||
return;
|
||||
}
|
||||
RigidbodyType = rigidbodyType;
|
||||
m_Rigidbody2D = rigidbody2D;
|
||||
m_Rigidbody = rigidbody;
|
||||
NetworkTransform = networkTransform;
|
||||
|
||||
if (m_IsRigidbody2D && m_Rigidbody2D == null)
|
||||
{
|
||||
m_Rigidbody2D = GetComponent<Rigidbody2D>();
|
||||
|
||||
}
|
||||
else if (m_Rigidbody == null)
|
||||
{
|
||||
m_Rigidbody = GetComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
SetOriginalInterpolation();
|
||||
|
||||
if (NetworkTransform == null)
|
||||
{
|
||||
NetworkTransform = GetComponent<NetworkTransform>();
|
||||
}
|
||||
|
||||
if (NetworkTransform != null)
|
||||
{
|
||||
NetworkTransform.RegisterRigidbody(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new System.Exception($"[Missing {nameof(NetworkTransform)}] No {nameof(NetworkTransform)} is assigned or can be found during initialization!");
|
||||
}
|
||||
|
||||
if (AutoUpdateKinematicState)
|
||||
{
|
||||
SetIsKinematic(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the position of the Rigidbody
|
||||
/// </summary>
|
||||
/// <returns><see cref="Vector3"/></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector3 GetPosition()
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
return m_Rigidbody2D.position;
|
||||
}
|
||||
else
|
||||
{
|
||||
return m_Rigidbody.position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rotation of the Rigidbody
|
||||
/// </summary>
|
||||
/// <returns><see cref="Quaternion"/></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Quaternion GetRotation()
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
var quaternion = Quaternion.identity;
|
||||
var angles = quaternion.eulerAngles;
|
||||
angles.z = m_Rigidbody2D.rotation;
|
||||
quaternion.eulerAngles = angles;
|
||||
return quaternion;
|
||||
}
|
||||
else
|
||||
{
|
||||
return m_Rigidbody.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the rigid body
|
||||
/// </summary>
|
||||
/// <param name="position">The <see cref="Vector3"/> position to move towards</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MovePosition(Vector3 position)
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.MovePosition(position);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.MovePosition(position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directly applies a position (like teleporting)
|
||||
/// </summary>
|
||||
/// <param name="position"><see cref="Vector3"/> position to apply to the Rigidbody</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetPosition(Vector3 position)
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.position = position;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the rotation and position of the <see cref="GameObject"/>'s <see cref="Transform"/>
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ApplyCurrentTransform()
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.position = transform.position;
|
||||
m_Rigidbody2D.rotation = transform.eulerAngles.z;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.position = transform.position;
|
||||
m_Rigidbody.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotatates the Rigidbody towards a specified rotation
|
||||
/// </summary>
|
||||
/// <param name="rotation">The rotation expressed as a <see cref="Quaternion"/></param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MoveRotation(Quaternion rotation)
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
var quaternion = Quaternion.identity;
|
||||
var angles = quaternion.eulerAngles;
|
||||
angles.z = m_Rigidbody2D.rotation;
|
||||
quaternion.eulerAngles = angles;
|
||||
m_Rigidbody2D.MoveRotation(quaternion);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.MoveRotation(rotation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a rotation to the Rigidbody
|
||||
/// </summary>
|
||||
/// <param name="rotation">The rotation to apply expressed as a <see cref="Quaternion"/></param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetRotation(Quaternion rotation)
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.rotation = rotation.eulerAngles.z;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.rotation = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the original interpolation of the Rigidbody while taking the Rigidbody type into consideration
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetOriginalInterpolation()
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
switch (m_Rigidbody2D.interpolation)
|
||||
{
|
||||
case RigidbodyInterpolation2D.None:
|
||||
{
|
||||
m_OriginalInterpolation = InterpolationTypes.None;
|
||||
break;
|
||||
}
|
||||
case RigidbodyInterpolation2D.Interpolate:
|
||||
{
|
||||
m_OriginalInterpolation = InterpolationTypes.Interpolate;
|
||||
break;
|
||||
}
|
||||
case RigidbodyInterpolation2D.Extrapolate:
|
||||
{
|
||||
m_OriginalInterpolation = InterpolationTypes.Extrapolate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (m_Rigidbody.interpolation)
|
||||
{
|
||||
case RigidbodyInterpolation.None:
|
||||
{
|
||||
m_OriginalInterpolation = InterpolationTypes.None;
|
||||
break;
|
||||
}
|
||||
case RigidbodyInterpolation.Interpolate:
|
||||
{
|
||||
m_OriginalInterpolation = InterpolationTypes.Interpolate;
|
||||
break;
|
||||
}
|
||||
case RigidbodyInterpolation.Extrapolate:
|
||||
{
|
||||
m_OriginalInterpolation = InterpolationTypes.Extrapolate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wakes the Rigidbody if it is sleeping
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WakeIfSleeping()
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
if (m_Rigidbody2D.IsSleeping())
|
||||
{
|
||||
m_Rigidbody2D.WakeUp();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_Rigidbody.IsSleeping())
|
||||
{
|
||||
m_Rigidbody.WakeUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the Rigidbody to sleep
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SleepRigidbody()
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.Sleep();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.Sleep();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsKinematic()
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
return m_Rigidbody2D.isKinematic;
|
||||
}
|
||||
else
|
||||
{
|
||||
return m_Rigidbody.isKinematic;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the kinematic state of the Rigidbody and handles updating the Rigidbody's
|
||||
/// interpolation setting based on the Kinematic state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When using the Rigidbody for <see cref="NetworkTransform"/> motion, this automatically
|
||||
/// adjusts from extrapolation to interpolation if:
|
||||
/// - The Rigidbody was originally set to extrapolation
|
||||
/// - The NetworkTransform is set to interpolate
|
||||
/// When the two above conditions are true:
|
||||
/// - When switching from non-kinematic to kinematic this will automatically
|
||||
/// switch the Rigidbody from extrapolation to interpolate.
|
||||
/// - When switching from kinematic to non-kinematic this will automatically
|
||||
/// switch the Rigidbody from interpolation back to extrapolation.
|
||||
/// </remarks>
|
||||
/// <param name="isKinematic"></param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetIsKinematic(bool isKinematic)
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.isKinematic = isKinematic;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.isKinematic = isKinematic;
|
||||
}
|
||||
|
||||
// If we are not spawned, then exit early
|
||||
if (!IsSpawned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (UseRigidBodyForMotion)
|
||||
{
|
||||
// Only if the NetworkTransform is set to interpolate do we need to check for extrapolation
|
||||
if (NetworkTransform.Interpolate && m_OriginalInterpolation == InterpolationTypes.Extrapolate)
|
||||
{
|
||||
if (IsKinematic())
|
||||
{
|
||||
// If not already set to interpolate then set the Rigidbody to interpolate
|
||||
if (m_Rigidbody.interpolation == RigidbodyInterpolation.Extrapolate)
|
||||
{
|
||||
// Sleep until the next fixed update when switching from extrapolation to interpolation
|
||||
SleepRigidbody();
|
||||
SetInterpolation(InterpolationTypes.Interpolate);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Switch it back to the original interpolation if non-kinematic (doesn't require sleep).
|
||||
SetInterpolation(m_OriginalInterpolation);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetInterpolation(m_IsAuthority ? m_OriginalInterpolation : (NetworkTransform.Interpolate ? InterpolationTypes.None : m_OriginalInterpolation));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetInterpolation(InterpolationTypes interpolationType)
|
||||
{
|
||||
switch (interpolationType)
|
||||
{
|
||||
case InterpolationTypes.None:
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.interpolation = RigidbodyInterpolation.None;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InterpolationTypes.Interpolate:
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InterpolationTypes.Extrapolate:
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.Extrapolate;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Rigidbody.interpolation = RigidbodyInterpolation.Extrapolate;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ResetInterpolation()
|
||||
{
|
||||
SetInterpolation(m_OriginalInterpolation);
|
||||
}
|
||||
|
||||
protected override void OnOwnershipChanged(ulong previous, ulong current)
|
||||
{
|
||||
UpdateOwnershipAuthority();
|
||||
base.OnOwnershipChanged(previous, current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the authority based on whether it is server or owner authoritative
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Distributed authority sessions will always be owner authoritative.
|
||||
/// </remarks>
|
||||
internal void UpdateOwnershipAuthority()
|
||||
{
|
||||
if (NetworkManager.DistributedAuthorityMode)
|
||||
{
|
||||
// When in distributed authority mode, always use HasAuthority
|
||||
m_IsAuthority = HasAuthority;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (NetworkTransform.IsServerAuthoritative())
|
||||
{
|
||||
m_IsAuthority = NetworkManager.IsServer;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_IsAuthority = IsOwner;
|
||||
}
|
||||
}
|
||||
|
||||
if (AutoUpdateKinematicState)
|
||||
{
|
||||
SetIsKinematic(!m_IsAuthority);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
UpdateOwnershipAuthority();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (UseRigidBodyForMotion && HasAuthority)
|
||||
{
|
||||
DetachFromFixedJoint();
|
||||
NetworkRigidbodyConnections.Clear();
|
||||
}
|
||||
|
||||
// If we are automatically handling the kinematic state...
|
||||
if (AutoUpdateKinematicState || AutoSetKinematicOnDespawn)
|
||||
{
|
||||
// Turn off physics for the rigid body until spawned, otherwise
|
||||
// non-owners can run fixed updates before the first full
|
||||
// NetworkTransform update and physics will be applied (i.e. gravity, etc)
|
||||
SetIsKinematic(true);
|
||||
}
|
||||
SetInterpolation(m_OriginalInterpolation);
|
||||
}
|
||||
|
||||
// TODO: Possibly provide a NetworkJoint that allows for more options than fixed.
|
||||
// Rigidbodies do not have the concept of "local space", and as such using a fixed joint will hold the object
|
||||
// in place relative to the parent so jitter/stutter does not occur.
|
||||
// Alternately, users can affix the fixed joint to a child GameObject (without a rigid body) of the parent NetworkObject
|
||||
// and then add a NetworkTransform to that in order to get the parented child NetworkObject to move around in "local space"
|
||||
public FixedJoint FixedJoint { get; private set; }
|
||||
public FixedJoint2D FixedJoint2D { get; private set; }
|
||||
|
||||
internal System.Collections.Generic.List<NetworkRigidbodyBase> NetworkRigidbodyConnections = new System.Collections.Generic.List<NetworkRigidbodyBase>();
|
||||
internal NetworkRigidbodyBase ParentBody;
|
||||
|
||||
private bool m_FixedJoint2DUsingGravity;
|
||||
private bool m_OriginalGravitySetting;
|
||||
private float m_OriginalGravityScale;
|
||||
|
||||
/// <summary>
|
||||
/// When using a custom <see cref="NetworkRigidbodyBase"/>, this virtual method is invoked when the
|
||||
/// <see cref="FixedJoint"/> is created in the event any additional adjustments are needed.
|
||||
/// </summary>
|
||||
protected virtual void OnFixedJointCreated()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When using a custom <see cref="NetworkRigidbodyBase"/>, this virtual method is invoked when the
|
||||
/// <see cref="FixedJoint2D"/> is created in the event any additional adjustments are needed.
|
||||
/// </summary>
|
||||
protected virtual void OnFixedJoint2DCreated()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ApplyFixedJoint2D(NetworkRigidbodyBase bodyToConnect, Vector3 position, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true)
|
||||
{
|
||||
transform.position = position;
|
||||
m_Rigidbody2D.position = position;
|
||||
m_OriginalGravitySetting = bodyToConnect.m_Rigidbody.useGravity;
|
||||
m_FixedJoint2DUsingGravity = useGravity;
|
||||
|
||||
if (!useGravity)
|
||||
{
|
||||
m_OriginalGravityScale = m_Rigidbody2D.gravityScale;
|
||||
m_Rigidbody2D.gravityScale = 0.0f;
|
||||
}
|
||||
|
||||
if (zeroVelocity)
|
||||
{
|
||||
m_Rigidbody2D.velocity = Vector2.zero;
|
||||
m_Rigidbody2D.angularVelocity = 0.0f;
|
||||
}
|
||||
|
||||
FixedJoint2D = gameObject.AddComponent<FixedJoint2D>();
|
||||
FixedJoint2D.connectedBody = bodyToConnect.m_Rigidbody2D;
|
||||
OnFixedJoint2DCreated();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ApplyFixedJoint(NetworkRigidbodyBase bodyToConnectTo, Vector3 position, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true)
|
||||
{
|
||||
transform.position = position;
|
||||
m_Rigidbody.position = position;
|
||||
if (zeroVelocity)
|
||||
{
|
||||
m_Rigidbody.linearVelocity = Vector3.zero;
|
||||
m_Rigidbody.angularVelocity = Vector3.zero;
|
||||
}
|
||||
m_OriginalGravitySetting = m_Rigidbody.useGravity;
|
||||
m_Rigidbody.useGravity = useGravity;
|
||||
FixedJoint = gameObject.AddComponent<FixedJoint>();
|
||||
FixedJoint.connectedBody = bodyToConnectTo.m_Rigidbody;
|
||||
FixedJoint.connectedMassScale = connectedMassScale;
|
||||
FixedJoint.massScale = massScale;
|
||||
OnFixedJointCreated();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Authority Only:
|
||||
/// When invoked and not already attached to a fixed joint, this will connect two rigid bodies with <see cref="UseRigidBodyForMotion"/> enabled.
|
||||
/// Invoke this method on the rigid body you wish to attach to another (i.e. weapon to player, sticky bomb to player/object, etc).
|
||||
/// <seealso cref="FixedJoint"/>
|
||||
/// <seealso cref="FixedJoint2D"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Parenting relative:
|
||||
/// - This instance can be viewed as the child.
|
||||
/// - The <param name="objectToConnectTo"/> can be viewed as the parent.
|
||||
/// <br/>
|
||||
/// This is the recommended way, as opposed to parenting, to attached/detatch two rigid bodies to one another when <see cref="UseRigidBodyForMotion"/> is enabled.
|
||||
/// For more details on using <see cref="UnityEngine.FixedJoint"/> and <see cref="UnityEngine.FixedJoint2D"/>.
|
||||
/// <br/>
|
||||
/// This provides a simple joint solution between two rigid bodies and serves as an example. You can add different joint types by creating a customized/derived
|
||||
/// version of <see cref="NetworkRigidbodyBase"/>.
|
||||
/// </remarks>
|
||||
/// <param name="objectToConnectTo">The target object to attach to.</param>
|
||||
/// <param name="positionOfConnection">The position of the connection (i.e. where you want the object to be affixed).</param>
|
||||
/// <param name="connectedMassScale">The target object's mass scale relative to this object being attached.</param>
|
||||
/// <param name="massScale">This object's mass scale relative to the target object's.</param>
|
||||
/// <param name="useGravity">Determines if this object will have gravity applied to it along with the object you are connecting this one to (the default is to not use gravity for this object)</param>
|
||||
/// <param name="zeroVelocity">When true (the default), both linear and angular velocities of this object are set to zero.</param>
|
||||
/// <param name="teleportObject">When true (the default), this object will teleport itself to the position of connection.</param>
|
||||
/// <returns>true (success) false (failed)</returns>
|
||||
public bool AttachToFixedJoint(NetworkRigidbodyBase objectToConnectTo, Vector3 positionOfConnection, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true, bool teleportObject = true)
|
||||
{
|
||||
if (!UseRigidBodyForMotion)
|
||||
{
|
||||
Debug.LogError($"[{GetType().Name}] {name} does not have {nameof(UseRigidBodyForMotion)} set! Either enable {nameof(UseRigidBodyForMotion)} on this component or do not use a {nameof(FixedJoint)} when parenting under a {nameof(NetworkObject)}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsKinematic())
|
||||
{
|
||||
Debug.LogError($"[{GetType().Name}] {name} is currently kinematic! You cannot use a {nameof(FixedJoint)} with Kinematic bodies!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (objectToConnectTo != null)
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
ApplyFixedJoint2D(objectToConnectTo, positionOfConnection, connectedMassScale, massScale, useGravity, zeroVelocity);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyFixedJoint(objectToConnectTo, positionOfConnection, connectedMassScale, massScale, useGravity, zeroVelocity);
|
||||
}
|
||||
|
||||
ParentBody = objectToConnectTo;
|
||||
ParentBody.NetworkRigidbodyConnections.Add(this);
|
||||
if (teleportObject)
|
||||
{
|
||||
NetworkTransform.SetState(teleportDisabled: false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void RemoveFromParentBody()
|
||||
{
|
||||
ParentBody.NetworkRigidbodyConnections.Remove(this);
|
||||
ParentBody = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authority Only:
|
||||
/// When invoked and already connected to an object via <see cref="FixedJoint"/> or <see cref="FixedJoint2D"/> (depending upon the type of rigid body),
|
||||
/// this will detach from the fixed joint and destroy the fixed joint component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the recommended way, as opposed to parenting, to attached/detatch two rigid bodies to one another when <see cref="UseRigidBodyForMotion"/> is enabled.
|
||||
/// </remarks>
|
||||
public void DetachFromFixedJoint()
|
||||
{
|
||||
if (!HasAuthority)
|
||||
{
|
||||
Debug.LogError($"[{name}] Only authority can invoke {nameof(DetachFromFixedJoint)}!");
|
||||
}
|
||||
if (UseRigidBodyForMotion)
|
||||
{
|
||||
if (m_IsRigidbody2D)
|
||||
{
|
||||
if (FixedJoint2D != null)
|
||||
{
|
||||
if (!m_FixedJoint2DUsingGravity)
|
||||
{
|
||||
FixedJoint2D.connectedBody.gravityScale = m_OriginalGravityScale;
|
||||
}
|
||||
FixedJoint2D.connectedBody = null;
|
||||
Destroy(FixedJoint2D);
|
||||
FixedJoint2D = null;
|
||||
ResetInterpolation();
|
||||
RemoveFromParentBody();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FixedJoint != null)
|
||||
{
|
||||
FixedJoint.connectedBody = null;
|
||||
m_Rigidbody.useGravity = m_OriginalGravitySetting;
|
||||
Destroy(FixedJoint);
|
||||
FixedJoint = null;
|
||||
ResetInterpolation();
|
||||
RemoveFromParentBody();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // COM_UNITY_MODULES_PHYSICS
|
||||
|
||||
11
Runtime/Components/NetworkRigidBodyBase.cs.meta
Normal file
11
Runtime/Components/NetworkRigidBodyBase.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c4434f0563fb7f42b3b2993c97ae81a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
Runtime/Components/NetworkRigidbody.cs
Normal file
21
Runtime/Components/NetworkRigidbody.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
#if COM_UNITY_MODULES_PHYSICS
|
||||
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 <see cref="Rigidbody"/> and disabling it on all peers but the authoritative one.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(NetworkTransform))]
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
[AddComponentMenu("Netcode/Network Rigidbody")]
|
||||
public class NetworkRigidbody : NetworkRigidbodyBase
|
||||
{
|
||||
protected virtual void Awake()
|
||||
{
|
||||
Initialize(RigidbodyTypes.Rigidbody);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // COM_UNITY_MODULES_PHYSICS
|
||||
11
Runtime/Components/NetworkRigidbody.cs.meta
Normal file
11
Runtime/Components/NetworkRigidbody.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6c0be61502bb534f922ebb746851216
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
Runtime/Components/NetworkRigidbody2D.cs
Normal file
21
Runtime/Components/NetworkRigidbody2D.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
#if COM_UNITY_MODULES_PHYSICS2D
|
||||
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(NetworkTransform))]
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
[AddComponentMenu("Netcode/Network Rigidbody 2D")]
|
||||
public class NetworkRigidbody2D : NetworkRigidbodyBase
|
||||
{
|
||||
protected virtual void Awake()
|
||||
{
|
||||
Initialize(RigidbodyTypes.Rigidbody2D);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // COM_UNITY_MODULES_PHYSICS2D
|
||||
11
Runtime/Components/NetworkRigidbody2D.cs.meta
Normal file
11
Runtime/Components/NetworkRigidbody2D.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80d7c879794dfda4687da0e400131852
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3754
Runtime/Components/NetworkTransform.cs
Normal file
3754
Runtime/Components/NetworkTransform.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Runtime/Components/NetworkTransform.cs.meta
Normal file
11
Runtime/Components/NetworkTransform.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e96cb6065543e43c4a752faaa1468eb1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
123
Runtime/Components/QuaternionCompressor.cs
Normal file
123
Runtime/Components/QuaternionCompressor.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Netcode
|
||||
{
|
||||
/// <summary>
|
||||
/// A Smallest Three Quaternion Compressor Implementation
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Explanation of why "The smallest three":
|
||||
/// Since a normalized Quaternion's unit value is 1.0f:
|
||||
/// x*x + y*y + z*z + w*w = M*M (where M is the magnitude of the vector)
|
||||
/// If w was the largest value and the quaternion is normalized:
|
||||
/// M = 1.0f (which M * M would still yield 1.0f)
|
||||
/// w*w = M*M - (x*x + y*y + z*z) or Mathf.Sqrt(1.0f - (x*x + y*y + z*z))
|
||||
/// w = Math.Sqrt(1.0f - (x*x + y*y + z*z))
|
||||
/// Using the largest number avoids potential loss of precision in the smallest three values.
|
||||
/// </remarks>
|
||||
public static class QuaternionCompressor
|
||||
{
|
||||
private const ushort k_PrecisionMask = (1 << 9) - 1;
|
||||
|
||||
// Square root of 2 over 2 (Mathf.Sqrt(2.0f) / 2.0f == 1.0f / Mathf.Sqrt(2.0f))
|
||||
// This provides encoding the smallest three components into a (+/-) Mathf.Sqrt(2.0f) / 2.0f range
|
||||
private const float k_SqrtTwoOverTwoEncoding = 0.70710678118654752440084436210485f;
|
||||
|
||||
// We can further improve the encoding compression by dividing k_SqrtTwoOverTwo into 1.0f and multiplying that
|
||||
// by the precision mask (minor reduction of runtime calculations)
|
||||
private const float k_CompressionEcodingMask = (1.0f / k_SqrtTwoOverTwoEncoding) * k_PrecisionMask;
|
||||
|
||||
// Used to shift the negative bit to the 10th bit position when compressing and encoding
|
||||
private const ushort k_ShiftNegativeBit = 9;
|
||||
|
||||
// We can do the same for our decoding and decompression by dividing k_PrecisionMask into 1.0 and multiplying
|
||||
// that by k_SqrtTwoOverTwo (minor reduction of runtime calculations)
|
||||
private const float k_DcompressionDecodingMask = (1.0f / k_PrecisionMask) * k_SqrtTwoOverTwoEncoding;
|
||||
|
||||
// The sign bit position (10th bit) used when decompressing and decoding
|
||||
private const ushort k_NegShortBit = 0x200;
|
||||
|
||||
// Negative bit set values
|
||||
private const ushort k_True = 1;
|
||||
private const ushort k_False = 0;
|
||||
|
||||
// Used to store the absolute value of the 4 quaternion elements
|
||||
private static Quaternion s_QuatAbsValues = Quaternion.identity;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses a Quaternion into an unsigned integer
|
||||
/// </summary>
|
||||
/// <param name="quaternion">the <see cref="Quaternion"/> to be compressed</param>
|
||||
/// <returns>the <see cref="Quaternion"/> compressed as an unsigned integer</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint CompressQuaternion(ref Quaternion quaternion)
|
||||
{
|
||||
// Store off the absolute value for each Quaternion element
|
||||
s_QuatAbsValues[0] = Mathf.Abs(quaternion[0]);
|
||||
s_QuatAbsValues[1] = Mathf.Abs(quaternion[1]);
|
||||
s_QuatAbsValues[2] = Mathf.Abs(quaternion[2]);
|
||||
s_QuatAbsValues[3] = Mathf.Abs(quaternion[3]);
|
||||
|
||||
// Get the largest element value of the quaternion to know what the remaining "Smallest Three" values are
|
||||
var quatMax = Mathf.Max(s_QuatAbsValues[0], s_QuatAbsValues[1], s_QuatAbsValues[2], s_QuatAbsValues[3]);
|
||||
|
||||
// Find the index of the largest element so we can skip that element while compressing and decompressing
|
||||
var indexToSkip = (ushort)(s_QuatAbsValues[0] == quatMax ? 0 : s_QuatAbsValues[1] == quatMax ? 1 : s_QuatAbsValues[2] == quatMax ? 2 : 3);
|
||||
|
||||
// Get the sign of the largest element which is all that is needed when calculating the sum of squares of a normalized quaternion.
|
||||
|
||||
var quatMaxSign = (quaternion[indexToSkip] < 0 ? k_True : k_False);
|
||||
|
||||
// Start with the index to skip which will be shifted to the highest two bits
|
||||
var compressed = (uint)indexToSkip;
|
||||
|
||||
// Step 1: Start with the first element
|
||||
var currentIndex = 0;
|
||||
|
||||
// Step 2: If we are on the index to skip preserve the current compressed value, otherwise proceed to step 3 and 4
|
||||
// Step 3: Get the sign of the element we are processing. If it is the not the same as the largest value's sign bit then we set the bit
|
||||
// Step 4: Get the compressed and encoded value by multiplying the absolute value of the current element by k_CompressionEcodingMask and round that result up
|
||||
compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
|
||||
currentIndex++;
|
||||
// Repeat the last 3 steps for the remaining elements
|
||||
compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
|
||||
currentIndex++;
|
||||
compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
|
||||
currentIndex++;
|
||||
compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
|
||||
|
||||
// Return the compress quaternion
|
||||
return compressed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress a compressed quaternion
|
||||
/// </summary>
|
||||
/// <param name="quaternion">quaternion to store the decompressed values within</param>
|
||||
/// <param name="compressed">the compressed quaternion</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void DecompressQuaternion(ref Quaternion quaternion, uint compressed)
|
||||
{
|
||||
// Get the last two bits for the index to skip (0-3)
|
||||
var indexToSkip = (int)(compressed >> 30);
|
||||
|
||||
// Reverse out the values while skipping over the largest value index
|
||||
var sumOfSquaredMagnitudes = 0.0f;
|
||||
for (int i = 3; i >= 0; --i)
|
||||
{
|
||||
if (i == indexToSkip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Check the negative bit and multiply that result with the decompressed and decoded value
|
||||
quaternion[i] = ((compressed & k_NegShortBit) > 0 ? -1.0f : 1.0f) * ((compressed & k_PrecisionMask) * k_DcompressionDecodingMask);
|
||||
sumOfSquaredMagnitudes += quaternion[i] * quaternion[i];
|
||||
compressed = compressed >> 10;
|
||||
}
|
||||
// Since a normalized quaternion's magnitude is 1.0f, we subtract the sum of the squared smallest three from the unit value and take
|
||||
// the square root of the difference to find the final largest value
|
||||
quaternion[indexToSkip] = Mathf.Sqrt(1.0f - sumOfSquaredMagnitudes);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Components/QuaternionCompressor.cs.meta
Normal file
11
Runtime/Components/QuaternionCompressor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb9d8b98d3c8bca469c8ee152353336f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
230
Runtime/Components/RigidbodyContactEventManager.cs
Normal file
230
Runtime/Components/RigidbodyContactEventManager.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
#if COM_UNITY_MODULES_PHYSICS
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Netcode.Components
|
||||
{
|
||||
public interface IContactEventHandler
|
||||
{
|
||||
Rigidbody GetRigidbody();
|
||||
|
||||
void ContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default);
|
||||
}
|
||||
|
||||
[AddComponentMenu("Netcode/Rigidbody Contact Event Manager")]
|
||||
public class RigidbodyContactEventManager : MonoBehaviour
|
||||
{
|
||||
public static RigidbodyContactEventManager Instance { get; private set; }
|
||||
|
||||
private struct JobResultStruct
|
||||
{
|
||||
public bool HasCollisionStay;
|
||||
public int ThisInstanceID;
|
||||
public int OtherInstanceID;
|
||||
public Vector3 AverageNormal;
|
||||
public Vector3 AverageCollisionStayNormal;
|
||||
public Vector3 ContactPoint;
|
||||
}
|
||||
|
||||
private NativeArray<JobResultStruct> m_ResultsArray;
|
||||
private int m_Count = 0;
|
||||
private JobHandle m_JobHandle;
|
||||
|
||||
private readonly Dictionary<int, Rigidbody> m_RigidbodyMapping = new Dictionary<int, Rigidbody>();
|
||||
private readonly Dictionary<int, IContactEventHandler> m_HandlerMapping = new Dictionary<int, IContactEventHandler>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
m_ResultsArray = new NativeArray<JobResultStruct>(16, Allocator.Persistent);
|
||||
Physics.ContactEvent += Physics_ContactEvent;
|
||||
if (Instance != null)
|
||||
{
|
||||
NetworkLog.LogError($"[Invalid][Multiple Instances] Found more than one instance of {nameof(RigidbodyContactEventManager)}: {name} and {Instance.name}");
|
||||
NetworkLog.LogError($"[Disable][Additional Instance] Disabling {name} instance!");
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public void RegisterHandler(IContactEventHandler contactEventHandler, bool register = true)
|
||||
{
|
||||
var rigidbody = contactEventHandler.GetRigidbody();
|
||||
var instanceId = rigidbody.GetInstanceID();
|
||||
if (register)
|
||||
{
|
||||
if (!m_RigidbodyMapping.ContainsKey(instanceId))
|
||||
{
|
||||
m_RigidbodyMapping.Add(instanceId, rigidbody);
|
||||
}
|
||||
|
||||
if (!m_HandlerMapping.ContainsKey(instanceId))
|
||||
{
|
||||
m_HandlerMapping.Add(instanceId, contactEventHandler);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_RigidbodyMapping.Remove(instanceId);
|
||||
m_HandlerMapping.Remove(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
m_JobHandle.Complete();
|
||||
m_ResultsArray.Dispose();
|
||||
|
||||
Physics.ContactEvent -= Physics_ContactEvent;
|
||||
|
||||
m_RigidbodyMapping.Clear();
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
private bool m_HasCollisions;
|
||||
private int m_CurrentCount = 0;
|
||||
|
||||
private void ProcessCollisions()
|
||||
{
|
||||
// Process all collisions
|
||||
for (int i = 0; i < m_Count; i++)
|
||||
{
|
||||
var thisInstanceID = m_ResultsArray[i].ThisInstanceID;
|
||||
var otherInstanceID = m_ResultsArray[i].OtherInstanceID;
|
||||
var rb0Valid = thisInstanceID != 0 && m_RigidbodyMapping.ContainsKey(thisInstanceID);
|
||||
var rb1Valid = otherInstanceID != 0 && m_RigidbodyMapping.ContainsKey(otherInstanceID);
|
||||
// Only notify registered rigid bodies.
|
||||
if (!rb0Valid || !rb1Valid || !m_HandlerMapping.ContainsKey(thisInstanceID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (m_ResultsArray[i].HasCollisionStay)
|
||||
{
|
||||
m_HandlerMapping[thisInstanceID].ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, m_RigidbodyMapping[otherInstanceID], m_ResultsArray[i].ContactPoint, m_ResultsArray[i].HasCollisionStay, m_ResultsArray[i].AverageCollisionStayNormal);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_HandlerMapping[thisInstanceID].ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, m_RigidbodyMapping[otherInstanceID], m_ResultsArray[i].ContactPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
// Only process new collisions
|
||||
if (!m_HasCollisions && m_CurrentCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This assures we won't process the same collision
|
||||
// set after it has been processed.
|
||||
if (m_HasCollisions)
|
||||
{
|
||||
m_CurrentCount = m_Count;
|
||||
m_HasCollisions = false;
|
||||
m_JobHandle.Complete();
|
||||
}
|
||||
ProcessCollisions();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
m_CurrentCount = 0;
|
||||
}
|
||||
|
||||
private ulong m_EventId;
|
||||
private void Physics_ContactEvent(PhysicsScene scene, NativeArray<ContactPairHeader>.ReadOnly pairHeaders)
|
||||
{
|
||||
m_EventId++;
|
||||
m_HasCollisions = true;
|
||||
int n = pairHeaders.Length;
|
||||
if (m_ResultsArray.Length < n)
|
||||
{
|
||||
m_ResultsArray.Dispose();
|
||||
m_ResultsArray = new NativeArray<JobResultStruct>(Mathf.NextPowerOfTwo(n), Allocator.Persistent);
|
||||
}
|
||||
m_Count = n;
|
||||
var job = new GetCollisionsJob()
|
||||
{
|
||||
PairedHeaders = pairHeaders,
|
||||
ResultsArray = m_ResultsArray
|
||||
};
|
||||
m_JobHandle = job.Schedule(n, 256);
|
||||
}
|
||||
|
||||
private struct GetCollisionsJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly]
|
||||
public NativeArray<ContactPairHeader>.ReadOnly PairedHeaders;
|
||||
|
||||
public NativeArray<JobResultStruct> ResultsArray;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
Vector3 averageNormal = Vector3.zero;
|
||||
Vector3 averagePoint = Vector3.zero;
|
||||
Vector3 averageCollisionStay = Vector3.zero;
|
||||
int count = 0;
|
||||
int collisionStaycount = 0;
|
||||
int positionCount = 0;
|
||||
for (int j = 0; j < PairedHeaders[index].pairCount; j++)
|
||||
{
|
||||
ref readonly var pair = ref PairedHeaders[index].GetContactPair(j);
|
||||
|
||||
if (pair.isCollisionExit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int k = 0; k < pair.contactCount; k++)
|
||||
{
|
||||
ref readonly var contact = ref pair.GetContactPoint(k);
|
||||
averagePoint += contact.position;
|
||||
positionCount++;
|
||||
if (!pair.isCollisionStay)
|
||||
{
|
||||
averageNormal += contact.normal;
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
averageCollisionStay += contact.normal;
|
||||
collisionStaycount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count != 0)
|
||||
{
|
||||
averageNormal /= count;
|
||||
}
|
||||
|
||||
if (collisionStaycount != 0)
|
||||
{
|
||||
averageCollisionStay /= collisionStaycount;
|
||||
}
|
||||
|
||||
if (positionCount != 0)
|
||||
{
|
||||
averagePoint /= positionCount;
|
||||
}
|
||||
|
||||
var result = new JobResultStruct()
|
||||
{
|
||||
ThisInstanceID = PairedHeaders[index].bodyInstanceID,
|
||||
OtherInstanceID = PairedHeaders[index].otherBodyInstanceID,
|
||||
AverageNormal = averageNormal,
|
||||
HasCollisionStay = collisionStaycount != 0,
|
||||
AverageCollisionStayNormal = averageCollisionStay,
|
||||
ContactPoint = averagePoint
|
||||
};
|
||||
|
||||
ResultsArray[index] = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Runtime/Components/RigidbodyContactEventManager.cs.meta
Normal file
11
Runtime/Components/RigidbodyContactEventManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 739e5cee846b6384988f9a47e4691836
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user