Skip to content

Instantly share code, notes, and snippets.

@liamcary
Last active November 28, 2022 10:53
Show Gist options
  • Save liamcary/628e5c4833ae0ba18c2c570992aeb993 to your computer and use it in GitHub Desktop.
Save liamcary/628e5c4833ae0ba18c2c570992aeb993 to your computer and use it in GitHub Desktop.
A script to improve performance of RealtimeTransforms in Rigidbody mode. This is a prototype, I haven't tested it thoroughly for production.
using Normal.Realtime;
using System;
using System.Collections;
using System.Reflection;
using UnityEngine;
namespace Normcore.Realtime
{
/// <summary>
/// This script is intended to be attached to an object with a RealtimeTransform in Rigidbody mode. We have to use reflection
/// to override the behaviour because the relevant normcore methods/classes are private/internal/sealed/etc.
///
/// RealtimeTransform's FixedUpdate method when using the Rigidbody strategy performs some operations multiple
/// times instead of caching and re-using the results. For one object its a small amount, but when you have dozens
/// of objects and multiple fixed updates running in one frame it can add up quickly.
///
/// While profiling i saw that I had 83 realtime transforms with rigidbodies and on average 2 fixed updates per frame.
/// The default behaviour accesses RealtimeModel isOwnedLocallySelf and ownerIdSelf twice each, which is already 332 calls.
/// This was taking .76ms on my high-end development PC. I didn't test on quest, because its already too high.
///
/// I added this script to some of the realtime transforms in the scene to roughly compare values in the profiler.
/// In a frame with three fixed updates, 30 instances using the original RealtimeTransform took 0.37ms to complete.
/// In that same frame, 54 instances with this script added took 0.21ms to complete. It's a small sample size and imprecise
/// metrics, but thats roughly 3 or more times faster if you scale to the same quantity. It would likely be faster again if
/// this was using normcore methods directly instead of using reflection.
/// </summary>
[DefaultExecutionOrder(-94)] // One higher than RealtimeTransform
public class RealtimeTransformOptimizer : MonoBehaviour
{
[SerializeField] RealtimeTransform _realtimeTransform;
[SerializeField] RealtimeView _realtimeView;
[SerializeField] Rigidbody _rigidbody;
bool _isInitialized;
int _clientId = -1;
bool _isAlreadyOwned;
RealtimeTransformModel _realtimeTransformModel;
Action _incrementFixedRoomTime;
Action<RealtimeTransformModel> _remoteFixedUpdate;
static bool _isStaticInitialized;
static FieldInfo _strategyField;
static PropertyInfo _modelProperty;
static MethodInfo _incrementMethod;
static MethodInfo _remoteFixedUpdateMethod;
void Reset()
{
_realtimeTransform = GetComponent<RealtimeTransform>();
_realtimeView = GetComponent<RealtimeView>();
_rigidbody = GetComponent<Rigidbody>();
}
void OnEnable()
{
if (_realtimeTransform == null) {
enabled = false; // Support stripping realtime components at runtime
} else {
_realtimeTransform.StopAllCoroutines();
}
}
void Awake()
{
_realtimeView.didReplaceAllComponentModels += HandleReplacedAllComponentModels;
}
void OnDestroy()
{
if (_realtimeView != null) {
_realtimeView.didReplaceAllComponentModels -= HandleReplacedAllComponentModels;
}
if (_realtimeTransform != null && _realtimeTransform.realtime != null) {
_realtimeTransform.realtime.didConnectToRoom -= HandleConnected;
_realtimeTransform.realtime.didDisconnectFromRoom -= HandleDisconnected;
}
}
void FixedUpdate()
{
if (!_isInitialized || _clientId == -1) {
return;
}
_incrementFixedRoomTime();
if (_realtimeTransform.ownerIDSelf == _clientId) {
if (!_isAlreadyOwned) {
_rigidbody.WakeUp();
_isAlreadyOwned = true;
}
} else {
_remoteFixedUpdate(_realtimeTransformModel);
_isAlreadyOwned = false;
}
}
void HandleConnected(Normal.Realtime.Realtime realtime)
{
_clientId = realtime.clientID;
}
void HandleDisconnected(Normal.Realtime.Realtime realtime)
{
_clientId = -1;
}
void HandleReplacedAllComponentModels(RealtimeView view)
{
object strategyValue;
if (!_isStaticInitialized) {
_strategyField = _realtimeTransform.GetType().GetField("_strategy", BindingFlags.Instance | BindingFlags.NonPublic);
_modelProperty = _realtimeTransform.GetType().GetProperty("model", BindingFlags.Instance | BindingFlags.NonPublic);
strategyValue = _strategyField.GetValue(_realtimeTransform);
_incrementMethod = strategyValue.GetType().GetMethod("IncrementFixedRoomTime", BindingFlags.Instance | BindingFlags.NonPublic);
_remoteFixedUpdateMethod = strategyValue.GetType().GetMethod("RemoteFixedUpdate", BindingFlags.Instance | BindingFlags.NonPublic);
_isStaticInitialized = true;
} else {
strategyValue = _strategyField.GetValue(_realtimeTransform);
}
_realtimeTransformModel = (RealtimeTransformModel) _modelProperty.GetValue(_realtimeTransform);
_incrementFixedRoomTime = (Action) Delegate.CreateDelegate(typeof(Action), strategyValue, _incrementMethod, true);
_remoteFixedUpdate = (Action<RealtimeTransformModel>) Delegate.CreateDelegate(typeof(Action<RealtimeTransformModel>), strategyValue, _remoteFixedUpdateMethod, true);
_realtimeTransform.realtime.didConnectToRoom += HandleConnected;
_realtimeTransform.realtime.didDisconnectFromRoom += HandleDisconnected;
_clientId = _realtimeTransform.realtime.clientID;
if (_realtimeTransform.ownerIDSelf == _clientId) {
_isAlreadyOwned = true;
_rigidbody.WakeUp();
}
_isInitialized = true;
}
}
}
@liamcary
Copy link
Author

Updated with a few improvements:

  • Cache static references to some of the methods and properties accessed via reflection.
  • Use FixedUpdate instead of a coroutine using WaitForFixedUpdate (note: this changes the order of execution but hasn't caused any issue in my tests https://docs.unity3d.com/Manual/ExecutionOrder.html)
  • Handle disconnects
  • Support null references for the realtime components to handle cases where realtime components are stripped at runtime.

This has been reliable for Realtime prefab instances in my project, but I haven't tested SceneView RealtimeTransforms with Rigidbodies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment