Skip to content

Instantly share code, notes, and snippets.

@Lachee
Created January 31, 2019 05:01
Show Gist options
  • Save Lachee/0d5ca7a4f4e9a19a35d878b3fc815834 to your computer and use it in GitHub Desktop.
Save Lachee/0d5ca7a4f4e9a19a35d878b3fc815834 to your computer and use it in GitHub Desktop.
Conversion of a script I use to use for all my rigidbodies in legacy networking.
using UnityEngine;
namespace Mirror
{
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkRigidbody2D")]
[RequireComponent(typeof(Rigidbody2D))]
public class NetworkRigidbody2D : NetworkBehaviour
{
private const int BUFFER_SIZE = 50;
public double interpolationBackTime = 0.1;
public double extrapolationLimit = 0.5;
[Tooltip("Compresses 16 Byte Quaternion into None=12, Some=6, Much=3, Lots=2 Byte")]
[SerializeField] Compression compressRotation = Compression.Much;
public enum Compression { None, Much, Lots }; // easily understandable and funny
[Header("Debug")]
private new Rigidbody2D rigidbody;
private bool _kinematic = false;
private State[] _bufferedStates = new State[BUFFER_SIZE]; // We store twenty states with "playback" information
private int _bufferCount; // Keep track of what slots are used
private float _lastClientSyncTime = 0;
[System.Serializable]
private struct State
{
public double timestamp;
public Vector3 position;
public Vector3 velocity;
public float rotation;
public float angularVelocity;
}
private void Start()
{
//Fetch the rigidbody
rigidbody = GetComponent<Rigidbody2D>();
}
[Command]
private void CmdClientToServerSync(byte[] payload)
{
// local authority client sends sync message to server for broadcasting
// deserialize payload
NetworkReader reader = new NetworkReader(payload);
Deserialize(reader);
// set dirty so that OnSerialize broadcasts it
SetDirtyBit(1UL);
}
public override bool OnSerialize(NetworkWriter writer, bool initialState)
{
Serialize(writer);
return true;
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
Deserialize(reader);
}
public void Serialize(NetworkWriter writer) {
//Update the rigidbody if its null.
if (rigidbody == null)
rigidbody = GetComponent<Rigidbody2D>();
try
{
//Position / Velocity
writer.Write(rigidbody.position);
writer.Write(rigidbody.velocity);
//Rotation / Angular Velocity
switch (compressRotation)
{
case Compression.None:
writer.Write(rigidbody.rotation);
writer.Write(rigidbody.angularVelocity);
break;
case Compression.Much:
writer.Write(Utils.ScaleFloatToByte(rigidbody.rotation, 0, 360, byte.MinValue, byte.MaxValue));
writer.Write(Utils.ScaleFloatToByte(rigidbody.angularVelocity, 0, 360, byte.MinValue, byte.MaxValue));
break;
case Compression.Lots:
writer.Write(Utils.PackThreeFloatsIntoUShort(rigidbody.rotation, rigidbody.angularVelocity, 0, 0, 360));
break;
}
}
catch (System.Exception e)
{
Debug.LogError("Exception while serializing!");
Debug.LogException(e);
}
}
public void Deserialize(NetworkReader reader)
{
//Update the rigidbody if its null.
if (rigidbody == null)
rigidbody = GetComponent<Rigidbody2D>();
try
{
//Prepare the state
State state = new State();
state.timestamp = NetworkTime.time;
//Validate the kinematic
//_kinematic = rigidbody.isKinematic = reader.ReadBoolean();
//Position / Velocity
state.position = reader.ReadVector2();
state.velocity = reader.ReadVector2();
//Rotation / Angular Velocity
switch(compressRotation)
{
case Compression.None:
state.rotation = reader.ReadSingle();
state.angularVelocity = reader.ReadSingle();
break;
case Compression.Much:
state.rotation = Utils.ScaleByteToFloat(reader.ReadByte(), byte.MinValue, byte.MaxValue, 0, 360);
state.angularVelocity = Utils.ScaleByteToFloat(reader.ReadByte(), byte.MinValue, byte.MaxValue, 0, 360);
break;
case Compression.Lots:
float[] pack = Utils.UnpackUShortIntoThreeFloats(reader.ReadUInt16(), 0, 360);
state.rotation = pack[0];
state.angularVelocity = pack[1];
break;
}
// Shift the buffer sideways, deleting state 20
for (int i = _bufferedStates.Length - 1; i >= 1; i--)
{
_bufferedStates[i] = _bufferedStates[i - 1];
}
//Update the first buffer
_bufferedStates[0] = state;
// Update used slot count, however never exceed the buffer size
// Slots aren't actually freed so this just makes sure the buffer is
// filled up and that uninitalized slots aren't used.
_bufferCount = Mathf.Min(_bufferCount + 1, _bufferedStates.Length);
// Check if states are in order, if it is inconsistent you could reshuffel or
// drop the out-of-order state. Nothing is done here
for (int i = 0; i < _bufferCount - 1; i++)
{
if (_bufferedStates[i].timestamp < _bufferedStates[i + 1].timestamp)
Debug.LogWarning("State inconsistent");
}
}
catch (System.Exception e)
{
Debug.LogError("Exception while deserializing!");
Debug.LogException(e);
}
}
/// <summary>
/// Should the rigidbody be synced?
/// </summary>
/// <returns></returns>
public bool ShouldSync() { return rigidbody.IsAwake(); }
void Update()
{
if (isServer)
{
//If we are the server and the rigidbody isnt sleeping, then say we are dirty.
SetDirtyBit(ShouldSync() ? 1UL : 0UL);
}
if (isClient)
{
if (!isServer && hasAuthority)
{
// check only each 'syncInterval'
if (Time.time - _lastClientSyncTime >= syncInterval)
{
if (ShouldSync())
{
// serialize
NetworkWriter writer = new NetworkWriter();
Serialize(writer);
// send to server
CmdClientToServerSync(writer.ToArray());
}
_lastClientSyncTime = Time.time;
}
}
}
if (!hasAuthority)
{
// This is the target playback time of the rigid body
double interpolationTime = NetworkTime.time - interpolationBackTime;
// Use interpolation if the target playback time is present in the buffer
if (_bufferedStates[0].timestamp > interpolationTime)
{
// Go through buffer and find correct state to play back
for (int i = 0; i < _bufferCount; i++)
{
if (_bufferedStates[i].timestamp <= interpolationTime || i == _bufferCount - 1)
{
// The state one slot newer (<100ms) than the best playback state
State rhs = _bufferedStates[Mathf.Max(i - 1, 0)];
// The best playback state (closest to 100 ms old (default time))
State lhs = _bufferedStates[i];
// Use the time between the two slots to determine if interpolation is necessary
double length = rhs.timestamp - lhs.timestamp;
float t = 0.0F;
// As the time difference gets closer to 100 ms t gets closer to 1 in
// which case rhs is only used
// Example:
// Time is 10.000, so sampleTime is 9.900
// lhs.time is 9.910 rhs.time is 9.980 length is 0.070
// t is 9.900 - 9.910 / 0.070 = 0.14. So it uses 14% of rhs, 86% of lhs
if (length > 0.0001)
t = (float)((interpolationTime - lhs.timestamp) / length);
// if t=0 => lhs is used directly
rigidbody.position = Vector3.Lerp(lhs.position, rhs.position, t);
rigidbody.rotation = Mathf.Lerp(lhs.rotation, rhs.rotation, t);
rigidbody.velocity = Vector3.Lerp(lhs.velocity, rhs.velocity, t);
rigidbody.angularVelocity = Mathf.Lerp(lhs.angularVelocity, rhs.angularVelocity, t);
return;
}
}
}
// Use extrapolation
else
{
State latest = _bufferedStates[0];
float extrapolationLength = (float)(interpolationTime - latest.timestamp);
// Don't extrapolation for more than 500 ms, you would need to do that carefully
if (extrapolationLength < extrapolationLimit && GetComponent<Rigidbody>() != null && !GetComponent<Rigidbody>().isKinematic)
{
rigidbody.position = latest.position + latest.velocity * extrapolationLength;
rigidbody.rotation = latest.rotation + latest.angularVelocity * extrapolationLength;
rigidbody.velocity = latest.velocity;
rigidbody.angularVelocity = latest.angularVelocity;
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment