holopy3/Assets/Normal/Realtime/Components/RealtimeTransform.cs
2020-12-10 15:25:12 +01:00

339 lines
18 KiB
C#

using UnityEngine;
using Normal.Realtime;
namespace Normal.Realtime {
[DisallowMultipleComponent]
public class RealtimeTransform : RealtimeComponent {
[SerializeField] private bool _syncPosition = true;
[SerializeField] private bool _syncRotation = true;
[SerializeField] private bool _syncScale = true;
[SerializeField] private bool _extrapolation = false;
public bool extrapolation { get { return _extrapolation; } set { _extrapolation = value; } }
private RealtimeTransformModel _model;
public RealtimeTransformModel model { get { return _model; } set { SetModel(value); } }
// TODO: Rather than getting getting the client ID from realtime to compare, I'd rather the model be able to tell us whether it's owned locally or not. The model should have a reference to the room/datastore it belongs to.
// Once IModel becomes a class instead of an interface, we'll be able to do this.
public int ownerID { get { return _model.ownerID; } }
public bool isOwnedLocally { get { return _model.ownerID == realtime.clientID; } }
public bool isOwnedByWorld { get { return _model.ownerID == -1; } }
private bool _wrotePoseChangesLastNetworkFrame = false;
private double _stopExtrapolatingAtRoomTime = -1.0; // TODO: Convert to float once we have a float value for room time since start
private Rigidbody _rigidbody;
// Unity events
private void Awake() {
_rigidbody = GetComponent<Rigidbody>();
}
void OnDestroy() {
// Clear model (unregisters from model events)
model = null;
}
private void FixedUpdate() {
ApplyRigidbody();
}
private void Update() {
ApplyTransform();
}
// Ownership
public void RequestOwnership() {
_model.RequestOwnership(realtime.clientID);
}
public void ClearOwnership() {
_model.ClearOwnership();
}
// Send transform
private void ModelWillWrite(RealtimeTransformModel model) {
if (model != _model) {
Debug.LogError("RealtimeTransform received model willWrite event from another model... This is a bug.");
return;
}
// If it's not owned by us, ignore the event.
if (!isOwnedLocally)
return;
// We own this RealtimeTransform. Update the model to reflect its current position.
if (_rigidbody != null) {
if (_syncPosition) _model.position = _rigidbody.position;
if (_syncRotation) _model.rotation = _rigidbody.rotation;
if (_syncScale) _model.scale = transform.localScale;
if (_syncPosition) _model.velocity = _rigidbody.velocity;
if (_syncRotation) _model.angularVelocity = _rigidbody.angularVelocity;
//if (_shouldSyncScale) _model.scaleVelocity = // ...
_model.useGravity = _rigidbody.useGravity;
_model.isKinematic = _rigidbody.isKinematic;
} else {
if (_syncPosition) _model.position = transform.localPosition;
if (_syncRotation) _model.rotation = transform.localRotation;
if (_syncScale) _model.scale = transform.localScale;
}
if (_model.ModelPoseChangesToSend()) {
// Only send the timestamp if we're sending other pose changes.
_model.timestamp = realtime.room.time;
_wrotePoseChangesLastNetworkFrame = true;
} else if (_wrotePoseChangesLastNetworkFrame) {
// If we wrote changes last frame, but this frame the model is clean. Force one more model update to go out so extrapolation calculates a zero velocity.
_model.timestamp = realtime.room.time;
}
// If the rigidbody has come to rest, clear ownership
if (_rigidbody != null && _rigidbody.IsSleeping() && !_rigidbody.isKinematic)
_model.ClearOwnership();
// Set should extrapolate flag
_model.shouldExtrapolate = _extrapolation;
}
// Receive transform
void ModelDidRead(RealtimeTransformModel model) {
if (model != _model) {
Debug.LogError("RealtimeTransform received model didRead event from another model... This is a bug.");
return;
}
// If it's owned by us, ignore the event.
if (isOwnedLocally)
return;
// Used to keep the value consistent after ownership change (also means the editor reflects the current state for remote transforms).
_extrapolation = _model.shouldExtrapolate;
// We've received a new frame, stop simulating locally now that we have more information on the collision.
_stopExtrapolatingAtRoomTime = -1.0f;
}
// Apply
private void ApplyTransform() {
if (_rigidbody != null)
return;
if (isOwnedLocally)
return;
// If the model is owned by someone, we extrapolate & lerp.
if (_model.ownerID != -1) {
// Snapshot
Vector3 position = _syncPosition ? _model.position : transform.localPosition;
Quaternion rotation = _syncRotation ? _model.rotation : transform.localRotation;
Vector3 scale = _syncScale ? _model.scale : transform.localScale;
bool shouldExtrapolate = _model.shouldExtrapolate;
double timestamp = _model.timestamp;
float deltaTime = (float)(realtime.room.time - timestamp);
// If the last snapshot is too old, skip extrapolation.
if (deltaTime > 1.0f)
shouldExtrapolate = false;
// Extrapolate
if (shouldExtrapolate) {
// Calculate velocity & angularVelocity
Vector3 velocity = Vector3.zero;
Vector3 angularVelocity = Vector3.zero;
Vector3 previousPosition = _model.previousPosition;
Quaternion previousRotation = _model.previousRotation;
double previousTimestamp = _model.previousTimestamp;
// Only extrapolate if we have a previous frame, otherwise we'll have no data to extrapolate with.
if (previousTimestamp > 0.0) {
Vector3 positionDelta = position - previousPosition;
Quaternion rotationDeltaQuaternion = Quaternion.Inverse(previousRotation) * rotation;
Vector3 rotationDeltaDegrees = ReduceAngles(rotationDeltaQuaternion.eulerAngles);
Vector3 rotationDelta = new Vector3(rotationDeltaDegrees.x * Mathf.Deg2Rad, rotationDeltaDegrees.y * Mathf.Deg2Rad, rotationDeltaDegrees.z * Mathf.Deg2Rad);
float timeDelta = (float)(timestamp - previousTimestamp);
if (timeDelta > 0.0f) {
velocity = positionDelta / timeDelta;
angularVelocity = rotationDelta / timeDelta;
}
// Cap extrapolation time
float extrapolateTimeMax = Time.fixedDeltaTime * 10.0f; // Limit to 5 frames.
deltaTime = Mathf.Clamp(deltaTime, 0.0f, extrapolateTimeMax);
// Simulate
PhysXEmulation.SimulatePhysX(ref position, ref rotation, ref velocity, ref angularVelocity, false, Vector3.zero, 0.0f, 0.0f, float.PositiveInfinity, float.PositiveInfinity, RigidbodyConstraints.None, deltaTime, Time.fixedDeltaTime);
}
}
// Apply
float lerpFactor = 25.0f;
if (_syncPosition) transform.localPosition = Vector3.Lerp(transform.localPosition, position, lerpFactor * Time.deltaTime);
if (_syncRotation) transform.localRotation = Quaternion.Slerp(transform.localRotation, rotation, lerpFactor * Time.deltaTime);
if (_syncScale) transform.localScale = Vector3.Lerp(transform.localScale, scale, lerpFactor * Time.deltaTime);
} else {
// If the model isn't owned by anyone, set the transform position instantly.
if (_syncPosition) transform.localPosition = _model.position;
if (_syncRotation) transform.localRotation = _model.rotation;
if (_syncScale) transform.localScale = _model.scale;
}
}
private void ApplyRigidbody() {
if (_rigidbody == null)
return;
if (isOwnedLocally)
return;
// If we're using rigidbody extrapolation and we've collided with something, let the local rigidbody simulation run until we receive another packet or 500ms has passed
if (_rigidbody != null && _extrapolation && _stopExtrapolatingAtRoomTime >= 0.0 && (realtime.room.time - _stopExtrapolatingAtRoomTime) < 0.5f)
return;
if (_rigidbody.useGravity != _model.useGravity)
_rigidbody.useGravity = _model.useGravity;
if (_rigidbody.isKinematic != _model.isKinematic)
_rigidbody.isKinematic = _model.isKinematic;
// If the model is owned by someone, we extrapolate & lerp.
if (_model.ownerID != -1) {
// Snapshot
Vector3 position = _syncPosition ? _model.position : _rigidbody.position;
Quaternion rotation = _syncRotation ? _model.rotation : _rigidbody.rotation;
Vector3 scale = _syncScale ? _model.scale : transform.localScale;
Vector3 velocity = _model.velocity;
Vector3 angularVelocity = _model.angularVelocity;
bool shouldExtrapolate = _model.shouldExtrapolate;
bool useGravity = _model.useGravity;
double timestamp = _model.timestamp;
float deltaTime = (float)(realtime.room.time - timestamp);
// If the last snapshot is too old, skip extrapolation.
if (deltaTime > 1.0f)
shouldExtrapolate = false;
// Extrapolate
if (shouldExtrapolate) {
// Cap extrapolation time
float extrapolateTimeMax = _model.isKinematic ? Time.fixedDeltaTime * 5.0f : 0.5f; // Limit to 5 frames if kinematic. 500ms if non-kinematic.
deltaTime = Mathf.Clamp(deltaTime, 0.0f, extrapolateTimeMax);
// Simulate
// TODO: I suspect that this function is off even though I've checked it. I know the single frame one is good, but I'm worried this one is off which is causing switching to the local simulation on bounce to not work correctly.
PhysXEmulation.SimulatePhysX(ref position, ref rotation, ref velocity, ref angularVelocity, useGravity, Physics.gravity, _rigidbody.drag, _rigidbody.angularDrag, _rigidbody.maxDepenetrationVelocity, _rigidbody.maxAngularVelocity, _rigidbody.constraints, deltaTime, Time.fixedDeltaTime);
}
// Apply
float lerpFactor = 25.0f;
// Set scale before position/rotation otherwise scale will get reset.
if (_syncScale) transform.localScale = Vector3.Lerp( transform.localScale, scale, lerpFactor * Time.fixedDeltaTime);
if (_syncPosition) _rigidbody.MovePosition( Vector3.Lerp(_rigidbody.position, position, lerpFactor * Time.fixedDeltaTime));
if (_syncRotation) _rigidbody.MoveRotation(Quaternion.Slerp(_rigidbody.rotation, rotation, lerpFactor * Time.fixedDeltaTime));
} else {
// If the model isn't owned by anyone, set the transform position instantly.
if (_syncScale) transform.localScale = _model.scale;
if (_syncPosition) _rigidbody.position = _model.position;
if (_syncRotation) _rigidbody.rotation = _model.rotation;
if (_syncPosition) _rigidbody.velocity = Vector3.zero;
if (_syncRotation) _rigidbody.angularVelocity = Vector3.zero;
}
}
// Collisions
private void OnCollisionEnter(Collision collision) {
// If we're using extrapolation, start simulating locally in order to make the bounce smoother.
if (_extrapolation)
_stopExtrapolatingAtRoomTime = realtime.room.time;
// If we are currently the authoritative owner of this rigidbody and we've collided with a rigidbody that isn't owned by anyone, let's take over ownership and start simulating it.
if (isOwnedLocally) {
Rigidbody rigidbody = collision.rigidbody;
if (rigidbody != null) {
RealtimeTransform otherRealtimeTransform = rigidbody.GetComponent<RealtimeTransform>();
if (otherRealtimeTransform != null) {
// If the other realtime rigidbody isn't owned by anyone and it's not kinematic, take it over.
if (otherRealtimeTransform.isOwnedByWorld && !otherRealtimeTransform.model.isKinematic)
otherRealtimeTransform.RequestOwnership();
}
}
}
}
void SetModel(RealtimeTransformModel model) {
if (_model != null) {
// Clear events
_model.willWrite -= ModelWillWrite;
_model.didRead -= ModelDidRead;
}
_model = model;
if (_model != null) {
// Register for events
_model.willWrite += ModelWillWrite;
_model.didRead += ModelDidRead;
_model.shouldWritePosition = _syncPosition;
_model.shouldWriteRotation = _syncRotation;
_model.shouldWriteScale = _syncScale;
_model.shouldWriteVelocityMetadata = _rigidbody != null;
// Sync with the model
if (_model.freshModel) {
// If this is a fresh model, fill it with the current transform state
if (_rigidbody != null) {
if (_syncPosition) _model.position = _rigidbody.position;
if (_syncRotation) _model.rotation = _rigidbody.rotation;
if (_syncScale) _model.scale = transform.localScale;
if (_syncPosition) _model.velocity = _rigidbody.velocity;
if (_syncRotation) _model.angularVelocity = _rigidbody.angularVelocity;
//if (_shouldSyncScale) _model.scaleVelocity = // ...
if (_rigidbody != null) _model.useGravity = _rigidbody.useGravity;
if (_rigidbody != null) _model.isKinematic = _rigidbody.isKinematic;
} else {
if (_syncPosition) _model.position = transform.localPosition;
if (_syncRotation) _model.rotation = transform.localRotation;
if (_syncScale) _model.scale = transform.localScale;
}
} else {
// If this is not a fresh model, set the transform using the model
if (_rigidbody != null) {
if (_syncScale) transform.localScale = _model.scale;
if (_syncPosition) _rigidbody.position = _model.position;
if (_syncRotation) _rigidbody.rotation = _model.rotation;
if (_syncPosition) _rigidbody.velocity = _model.velocity;
if (_syncRotation) _rigidbody.angularVelocity = _model.angularVelocity;
if (_rigidbody != null) _rigidbody.useGravity = _model.useGravity;
if (_rigidbody != null) _rigidbody.isKinematic = _model.isKinematic;
} else {
if (_syncPosition) transform.localPosition = _model.position;
if (_syncRotation) transform.localRotation = _model.rotation;
if (_syncScale) transform.localScale = _model.scale;
}
}
}
}
// Utility
private static Vector3 ReduceAngles(Vector3 angles) {
// Cap all angles between -180 and 180
// X
if (angles.x > 180.0f) angles.x -= 360.0f;
if (angles.x < -180.0f) angles.x += 360.0f;
// Y
if (angles.y > 180.0f) angles.y -= 360.0f;
if (angles.y < -180.0f) angles.y += 360.0f;
// Z
if (angles.z > 180.0f) angles.z -= 360.0f;
if (angles.z < -180.0f) angles.z += 360.0f;
return angles;
}
}
}