diff --git a/QSB/QSB.csproj b/QSB/QSB.csproj
index b4f026b0..44931cfa 100644
--- a/QSB/QSB.csproj
+++ b/QSB/QSB.csproj
@@ -15,7 +15,7 @@
pdbonly
-
+
@@ -69,7 +69,7 @@ copy /y "$(OwmlDir)\OWML.Abstractions.dll" "$(UnityAssetsDir)"
-
+
diff --git a/QSB/Syncs/QSBNetworkTransform.cs b/QSB/Syncs/QSBNetworkTransform.cs
new file mode 100644
index 00000000..2af73244
--- /dev/null
+++ b/QSB/Syncs/QSBNetworkTransform.cs
@@ -0,0 +1,36 @@
+using Mirror;
+using QSB.Utility;
+using UnityEngine;
+
+namespace QSB.Syncs
+{
+ public class QSBNetworkTransform : QSBNetworkBehaviour
+ {
+ protected override float SendInterval => 0.05f;
+
+ protected Vector3 _prevPosition;
+ protected Quaternion _prevRotation;
+
+ protected override void UpdatePrevData()
+ {
+ _prevPosition = transform.position;
+ _prevRotation = transform.rotation;
+ }
+
+ protected override bool HasChanged() =>
+ Vector3.Distance(transform.position, _prevPosition) > 1E-05f ||
+ Quaternion.Angle(transform.rotation, _prevRotation) > 1E-05f;
+
+ protected override void Serialize(NetworkWriter writer, bool initialState)
+ {
+ writer.Write(transform.position);
+ writer.Write(transform.rotation);
+ }
+
+ protected override void Deserialize(NetworkReader reader, bool initialState)
+ {
+ transform.position = reader.ReadVector3();
+ transform.rotation = reader.ReadQuaternion();
+ }
+ }
+}
diff --git a/QSB/Utility/QSBNetworkBehaviour.cs b/QSB/Utility/QSBNetworkBehaviour.cs
new file mode 100644
index 00000000..36a4233b
--- /dev/null
+++ b/QSB/Utility/QSBNetworkBehaviour.cs
@@ -0,0 +1,102 @@
+using Mirror;
+using System;
+
+namespace QSB.Utility
+{
+ public abstract class QSBNetworkBehaviour : NetworkBehaviour
+ {
+ protected virtual float SendInterval => 0.1f;
+ protected virtual bool Unreliable => true;
+
+ private double _lastSendTime;
+
+ protected abstract bool HasChanged();
+ protected abstract void UpdatePrevData();
+ protected abstract void Serialize(NetworkWriter writer, bool initialState);
+ protected abstract void Deserialize(NetworkReader reader, bool initialState);
+
+ public override bool OnSerialize(NetworkWriter writer, bool initialState)
+ {
+ var changed = base.OnSerialize(writer, initialState);
+ if (initialState && isServer)
+ {
+ Serialize(writer, true);
+ }
+
+ return changed;
+ }
+
+ public override void OnDeserialize(NetworkReader reader, bool initialState)
+ {
+ base.OnDeserialize(reader, initialState);
+ if (initialState && !isServer)
+ {
+ UpdatePrevData();
+ Deserialize(reader, true);
+ }
+ }
+
+ protected virtual void Awake() => UpdatePrevData();
+
+ protected virtual void Update()
+ {
+ if (!isClient)
+ {
+ return;
+ }
+
+ if (!hasAuthority)
+ {
+ return;
+ }
+
+ if (!NetworkClient.ready)
+ {
+ return;
+ }
+
+ if (NetworkTime.localTime >= _lastSendTime + SendInterval)
+ {
+ _lastSendTime = NetworkTime.localTime;
+
+ if (!HasChanged())
+ {
+ return;
+ }
+
+ UpdatePrevData();
+ using var writer = NetworkWriterPool.GetWriter();
+ Serialize(writer, false);
+
+ var data = writer.ToArraySegment();
+ if (Unreliable)
+ {
+ CmdSendDataUnreliable(data);
+ }
+ else
+ {
+ CmdSendDataReliable(data);
+ }
+ }
+ }
+
+ [Command(channel = Channels.Unreliable, requiresAuthority = true)]
+ private void CmdSendDataUnreliable(ArraySegment data) => RpcSendDataUnreliable(data);
+
+ [ClientRpc(channel = Channels.Unreliable, includeOwner = false)]
+ private void RpcSendDataUnreliable(ArraySegment data) => OnData(data);
+
+ [Command(channel = Channels.Reliable, requiresAuthority = true)]
+ private void CmdSendDataReliable(ArraySegment data) => RpcSendDataReliable(data);
+
+ [ClientRpc(channel = Channels.Reliable, includeOwner = false)]
+ private void RpcSendDataReliable(ArraySegment data) => OnData(data);
+
+ private void OnData(ArraySegment data)
+ {
+ UpdatePrevData();
+ using var reader = NetworkReaderPool.GetReader(data);
+ Deserialize(reader, false);
+ }
+ }
+}