mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
quake, single commit
This commit is contained in:
parent
f9c47d7e91
commit
f4d76ca49f
@ -0,0 +1,115 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody (Unreliable Compressed)")]
|
||||
public class NetworkRigidbodyUnreliableCompressed : NetworkTransformUnreliableCompressed
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f830b261ed7644a4b1cc262cf36fc96
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -42,6 +42,16 @@ public class NetworkTransformReliable : NetworkTransformBase
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot last;
|
||||
|
||||
// validation //////////////////////////////////////////////////////////
|
||||
// Configure is called from OnValidate and Awake
|
||||
protected override void Configure()
|
||||
{
|
||||
base.Configure();
|
||||
|
||||
// force syncMethod to reliable
|
||||
syncMethod = SyncMethod.Reliable;
|
||||
}
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void Update()
|
||||
{
|
||||
|
@ -30,6 +30,16 @@ public class NetworkTransformUnreliable : NetworkTransformBase
|
||||
protected Changed cachedChangedComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
|
||||
// validation //////////////////////////////////////////////////////////
|
||||
// Configure is called from OnValidate and Awake
|
||||
protected override void Configure()
|
||||
{
|
||||
base.Configure();
|
||||
|
||||
// force syncMethod to unreliable
|
||||
syncMethod = SyncMethod.Unreliable;
|
||||
}
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
// Update applies interpolation
|
||||
void Update()
|
||||
|
@ -0,0 +1,480 @@
|
||||
// NetworkTransform V3 based on NetworkTransformUnreliable, using Mirror's new
|
||||
// Unreliable quake style networking model with delta compression.
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Unreliable Compressed)")]
|
||||
public class NetworkTransformUnreliableCompressed : NetworkTransformBase
|
||||
{
|
||||
[Header("Additional Settings")]
|
||||
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
|
||||
public float onlySyncOnChangeCorrectionMultiplier = 2;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float rotationSensitivity = 0.01f;
|
||||
|
||||
// delta compression is capable of detecting byte-level changes.
|
||||
// if we scale float position to bytes,
|
||||
// then small movements will only change one byte.
|
||||
// this gives optimal bandwidth.
|
||||
// benchmark with 0.01 precision: 130 KB/s => 60 KB/s
|
||||
// benchmark with 0.1 precision: 130 KB/s => 30 KB/s
|
||||
[Header("Precision")]
|
||||
[Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")]
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float positionPrecision = 0.01f; // 1 cm
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float rotationPrecision = 0.001f; // this is for the quaternion's components, needs to be small
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float scalePrecision = 0.01f; // 1 cm
|
||||
|
||||
[Header("Debug")]
|
||||
public bool debugDraw = false;
|
||||
|
||||
// delta compression needs to remember 'last' to compress against.
|
||||
// this is from reliable full state serializations, not from last
|
||||
// unreliable delta since that isn't guaranteed to be delivered.
|
||||
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
protected Vector4Long lastSerializedRotation = Vector4Long.zero;
|
||||
protected Vector4Long lastDeserializedRotation = Vector4Long.zero;
|
||||
|
||||
protected Vector3Long lastSerializedScale = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot last;
|
||||
|
||||
// validation //////////////////////////////////////////////////////////
|
||||
// Configure is called from OnValidate and Awake
|
||||
protected override void Configure()
|
||||
{
|
||||
base.Configure();
|
||||
|
||||
// force syncMethod to unreliable
|
||||
syncMethod = SyncMethod.Unreliable;
|
||||
|
||||
// Unreliable ignores syncInterval. don't need to force anymore:
|
||||
// sendIntervalMultiplier = 1;
|
||||
}
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServer();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClient();
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// It has to be checked in LateUpdate() for onlySyncOnChange to avoid
|
||||
// the possibility of Update() running first before the object's movement
|
||||
// script's Update(), which then causes NT to send every alternate frame
|
||||
// instead.
|
||||
if (isServer || (IsClientWithAuthority && NetworkClient.ready))
|
||||
{
|
||||
if (!onlySyncOnChange || Changed(Construct()))
|
||||
SetDirty();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateServer()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateClient()
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (!IsClientWithAuthority)
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
|
||||
if (debugDraw)
|
||||
{
|
||||
Debug.DrawLine(from.position, to.position, Color.white, 10f);
|
||||
Debug.DrawLine(computed.position, computed.position + Vector3.up, Color.white, 10f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if position / rotation / scale changed since last _full reliable_ sync.
|
||||
protected virtual bool Changed(TransformSnapshot current) =>
|
||||
// position is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.position, current.position, positionPrecision) ||
|
||||
// rotation isn't quantized / delta compressed.
|
||||
// check with sensitivity.
|
||||
Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity ||
|
||||
// scale is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.scale, current.scale, scalePrecision);
|
||||
|
||||
// helper function to compare quantized representations of a Vector3
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision)
|
||||
{
|
||||
Compression.ScaleToLong(u, precision, out Vector3Long uQuantized);
|
||||
Compression.ScaleToLong(v, precision, out Vector3Long vQuantized);
|
||||
return uQuantized != vQuantized;
|
||||
}
|
||||
|
||||
// Unreliable OnSerialize:
|
||||
// - initial=true sends reliable full state
|
||||
// - initial=false sends unreliable delta states
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// get current snapshot for broadcasting.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
// ClientToServer optimization:
|
||||
// for interpolated client owned identities,
|
||||
// always broadcast the latest known snapshot so other clients can
|
||||
// interpolate immediately instead of catching up too
|
||||
|
||||
// TODO dirty mask? [compression is very good w/o it already]
|
||||
// each vector's component is delta compressed.
|
||||
// an unchanged component would still require 1 byte.
|
||||
// let's use a dirty bit mask to filter those out as well.
|
||||
|
||||
// Debug.Log($"NT OnSerialize: initial={initialState} method={syncMethod}");
|
||||
|
||||
// reliable full state
|
||||
if (initialState)
|
||||
{
|
||||
// TODO initialState is now sent multiple times. find a new fix for this:
|
||||
// If there is a last serialized snapshot, we use it.
|
||||
// This prevents the new client getting a snapshot that is different
|
||||
// from what the older clients last got. If this happens, and on the next
|
||||
// regular serialisation the delta compression will get wrong values.
|
||||
// Notes:
|
||||
// 1. Interestingly only the older clients have it wrong, because at the end
|
||||
// of this function, last = snapshot which is the initial state's snapshot
|
||||
// 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate
|
||||
// snapshot constructed would have been the same as the last anyway.
|
||||
// if (last.remoteTime > 0) snapshot = last;
|
||||
|
||||
int startPosition = writer.Position;
|
||||
|
||||
if (syncPosition) writer.WriteVector3(snapshot.position);
|
||||
if (syncRotation)
|
||||
{
|
||||
// if smallest-three quaternion compression is enabled,
|
||||
// then we don't need baseline rotation since delta always
|
||||
// sends an absolute value.
|
||||
if (!compressRotation)
|
||||
{
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
}
|
||||
if (syncScale) writer.WriteVector3(snapshot.scale);
|
||||
|
||||
// save serialized as 'last' for next delta compression.
|
||||
// only for reliable full sync, since unreliable isn't guaranteed to arrive.
|
||||
if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
|
||||
if (syncRotation && !compressRotation) Compression.ScaleToLong(snapshot.rotation, rotationPrecision, out lastSerializedRotation);
|
||||
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
|
||||
// set 'last'
|
||||
last = snapshot;
|
||||
}
|
||||
// unreliable delta: compress against last full reliable state
|
||||
else
|
||||
{
|
||||
int startPosition = writer.Position;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
{
|
||||
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
|
||||
}
|
||||
else
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
// this works for quaternions too, where xyzw are [-1,1]
|
||||
// and gradually change as rotation changes.
|
||||
Compression.ScaleToLong(snapshot.rotation, rotationPrecision, out Vector4Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedRotation, quantized);
|
||||
}
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unreliable OnDeserialize:
|
||||
// - initial=true sends reliable full state
|
||||
// - initial=false sends unreliable delta states
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
Vector3? position = null;
|
||||
Quaternion? rotation = null;
|
||||
Vector3? scale = null;
|
||||
|
||||
// reliable full state
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition)
|
||||
{
|
||||
position = reader.ReadVector3();
|
||||
|
||||
if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.green, 10.0f);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// if smallest-three quaternion compression is enabled,
|
||||
// then we don't need baseline rotation since delta always
|
||||
// sends an absolute value.
|
||||
if (!compressRotation)
|
||||
{
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
}
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
|
||||
// save deserialized as 'last' for next delta compression.
|
||||
// only for reliable full sync, since unreliable isn't guaranteed to arrive.
|
||||
if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition);
|
||||
if (syncRotation && !compressRotation) Compression.ScaleToLong(rotation.Value, rotationPrecision, out lastDeserializedRotation);
|
||||
if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
}
|
||||
// unreliable delta: decompress against last full reliable state
|
||||
else
|
||||
{
|
||||
// varint -> delta -> quantize
|
||||
if (syncPosition)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
|
||||
position = Compression.ScaleToFloat(quantized, positionPrecision);
|
||||
|
||||
if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.yellow, 10.0f);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
{
|
||||
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
|
||||
}
|
||||
else
|
||||
{
|
||||
// varint -> delta -> quantize
|
||||
// this works for quaternions too, where xyzw are [-1,1]
|
||||
// and gradually change as rotation changes.
|
||||
Vector4Long quantized = DeltaCompression.Decompress(reader, lastDeserializedRotation);
|
||||
rotation = Compression.ScaleToFloat(quantized, rotationPrecision);
|
||||
}
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
|
||||
scale = Compression.ScaleToFloat(quantized, scalePrecision);
|
||||
}
|
||||
|
||||
// handle depending on server / client / host.
|
||||
// server has priority for host mode.
|
||||
//
|
||||
// only do this for the unreliable delta states!
|
||||
// processing the reliable baselines shows noticeable jitter
|
||||
// around baseline syncs (e.g. tanks demo @ 4 Hz sendRate).
|
||||
// unreliable deltas are always within the same time delta,
|
||||
// so this gives perfectly smooth results.
|
||||
if (isServer) OnClientToServerSync(position, rotation, scale);
|
||||
else if (isClient) OnServerToClientSync(position, rotation, scale);
|
||||
}
|
||||
}
|
||||
|
||||
// sync ////////////////////////////////////////////////////////////////
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeStamp,
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
clientSnapshots,
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval * sendIntervalMultiplier,
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// only sync on change /////////////////////////////////////////////////
|
||||
// snap interp. needs a continous flow of packets.
|
||||
// 'only sync on change' interrupts it while not changed.
|
||||
// once it restarts, snap interp. will interp from the last old position.
|
||||
// this will cause very noticeable stutter for the first move each time.
|
||||
// the fix is quite simple.
|
||||
|
||||
// 1. detect if the remaining snapshot is too old from a past move.
|
||||
static bool NeedsCorrection(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
double remoteTimestamp,
|
||||
double bufferTime,
|
||||
double toleranceMultiplier) =>
|
||||
snapshots.Count == 1 &&
|
||||
remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier;
|
||||
|
||||
// 2. insert a fake snapshot at current position,
|
||||
// exactly one 'sendInterval' behind the newly received one.
|
||||
static void RewriteHistory(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
// timestamp of packet arrival, not interpolated remote time!
|
||||
double remoteTimeStamp,
|
||||
double localTime,
|
||||
double sendInterval,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
Vector3 scale)
|
||||
{
|
||||
// clear the previous snapshot
|
||||
snapshots.Clear();
|
||||
|
||||
// insert a fake one at where we used to be,
|
||||
// 'sendInterval' behind the new one.
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
|
||||
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
position,
|
||||
rotation,
|
||||
scale
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// reset state for next session.
|
||||
// do not ever call this during a session (i.e. after teleport).
|
||||
// calling this will break delta compression.
|
||||
public override void ResetState()
|
||||
{
|
||||
base.ResetState();
|
||||
|
||||
// reset delta
|
||||
lastSerializedPosition = Vector3Long.zero;
|
||||
lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
lastSerializedRotation = Vector4Long.zero;
|
||||
lastDeserializedRotation = Vector4Long.zero;
|
||||
|
||||
lastSerializedScale = Vector3Long.zero;
|
||||
lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// reset 'last' for delta too
|
||||
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d993bac37a92145448c1ea027b3e9ddc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -11,7 +11,6 @@ GameObject:
|
||||
- component: {fileID: 4492442352427800}
|
||||
- component: {fileID: 114118589361100106}
|
||||
- component: {fileID: 114250499875391520}
|
||||
- component: {fileID: 3464953498043699706}
|
||||
- component: {fileID: 114654712548978148}
|
||||
- component: {fileID: 6900008319038825817}
|
||||
m_Layer: 0
|
||||
@ -31,6 +30,7 @@ Transform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 5803173220413450940}
|
||||
- {fileID: 2155495746218491392}
|
||||
@ -50,7 +50,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
sceneId: 0
|
||||
_assetId: 3454335836
|
||||
_assetId: 2638947628
|
||||
serverOnly: 0
|
||||
visibility: 0
|
||||
hasSpawned: 0
|
||||
@ -63,9 +63,10 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 1916082411674582}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: d993bac37a92145448c1ea027b3e9ddc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
syncMethod: 1
|
||||
syncDirection: 1
|
||||
syncMode: 0
|
||||
syncInterval: 0
|
||||
@ -79,47 +80,14 @@ MonoBehaviour:
|
||||
interpolateRotation: 1
|
||||
interpolateScale: 0
|
||||
coordinateSpace: 0
|
||||
timelineOffset: 0
|
||||
timelineOffset: 1
|
||||
showGizmos: 0
|
||||
showOverlay: 0
|
||||
overlayColor: {r: 0, g: 0, b: 0, a: 0.5}
|
||||
bufferResetMultiplier: 3
|
||||
positionSensitivity: 0.01
|
||||
onlySyncOnChangeCorrectionMultiplier: 2
|
||||
rotationSensitivity: 0.01
|
||||
scaleSensitivity: 0.01
|
||||
--- !u!114 &3464953498043699706
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1916082411674582}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
syncDirection: 1
|
||||
syncMode: 0
|
||||
syncInterval: 0
|
||||
target: {fileID: 5803173220413450936}
|
||||
syncPosition: 0
|
||||
syncRotation: 1
|
||||
syncScale: 0
|
||||
onlySyncOnChange: 1
|
||||
compressRotation: 1
|
||||
interpolatePosition: 0
|
||||
interpolateRotation: 1
|
||||
interpolateScale: 0
|
||||
coordinateSpace: 0
|
||||
timelineOffset: 0
|
||||
showGizmos: 0
|
||||
showOverlay: 0
|
||||
overlayColor: {r: 0, g: 0, b: 0, a: 0.5}
|
||||
bufferResetMultiplier: 3
|
||||
positionSensitivity: 0.01
|
||||
rotationSensitivity: 0.01
|
||||
scaleSensitivity: 0.01
|
||||
positionPrecision: 0.01
|
||||
scalePrecision: 0.01
|
||||
--- !u!114 &114654712548978148
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -132,6 +100,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 7deadf756194d461e9140e42d651693b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
syncMethod: 1
|
||||
syncDirection: 0
|
||||
syncMode: 0
|
||||
syncInterval: 0.1
|
||||
@ -196,6 +165,7 @@ Transform:
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 5, z: 0}
|
||||
m_LocalScale: {x: 0.1, y: 0.1, z: 0.1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4492442352427800}
|
||||
m_RootOrder: 1
|
||||
@ -211,10 +181,12 @@ MeshRenderer:
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 2
|
||||
m_RayTraceProcedural: 0
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
@ -239,6 +211,7 @@ MeshRenderer:
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!102 &955977906578811009
|
||||
TextMesh:
|
||||
serializedVersion: 3
|
||||
@ -337,9 +310,9 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3}
|
||||
--- !u!4 &5803173220413450940 stripped
|
||||
--- !u!4 &606281948174800110 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c,
|
||||
m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c,
|
||||
type: 3}
|
||||
m_PrefabInstance: {fileID: 7130959241934869977}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
@ -355,9 +328,9 @@ Transform:
|
||||
type: 3}
|
||||
m_PrefabInstance: {fileID: 7130959241934869977}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!4 &606281948174800110 stripped
|
||||
--- !u!4 &5803173220413450940 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c,
|
||||
m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c,
|
||||
type: 3}
|
||||
m_PrefabInstance: {fileID: 7130959241934869977}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
|
@ -20,10 +20,18 @@ public class Tank : NetworkBehaviour
|
||||
public Transform projectileMount;
|
||||
|
||||
[Header("Stats")]
|
||||
[SyncVar] public int health = 5;
|
||||
public int health = 5;
|
||||
int lastHealth = 5;
|
||||
|
||||
void Update()
|
||||
{
|
||||
// manual setdirty test
|
||||
if (health != lastHealth)
|
||||
{
|
||||
SetDirty();
|
||||
lastHealth = health;
|
||||
}
|
||||
|
||||
// always update health bar.
|
||||
// (SyncVar hook would only update on clients, not on server)
|
||||
healthBar.text = new string('-', health);
|
||||
@ -91,5 +99,17 @@ void RotateTurret()
|
||||
turret.transform.LookAt(lookRotation);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// Debug.LogWarning($"Tank {name} OnSerialize {(initialState ? "full" : "delta")} health={health}");
|
||||
writer.WriteInt(health);
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
health = reader.ReadInt();
|
||||
// Debug.LogWarning($"Tank {name} OnDeserialize {(initialState ? "full" : "delta")} health={health}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user