Skip to content

Instantly share code, notes, and snippets.

@liamcary
Last active October 8, 2024 01:42
A script to improve performance of RealtimeTransforms in Rigidbody mode. See comments for fixes required for the latest Normcore versions.
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);
// In the latest Normcore versions you need to access IncrementFixedRoomTime and RemoteFixedUpdate via the base type.
// Replace the above two lines with the following three lines if you get errors about methods not being able to be found.
// I haven't tested these changes myself, but thanks to Erenimooo and Utku from the normcore discord for providing this fix.
// var strategyBaseType = strategyValue.GetType().BaseType;
// _incrementMethod = strategyBaseType.GetMethod("IncrementFixedRoomTime", BindingFlags.Instance | BindingFlags.NonPublic);
// _remoteFixedUpdateMethod = strategyBaseType.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.

@liamcary
Copy link
Author

liamcary commented Oct 8, 2024

If you're using the latest normcore versions, you'll need to apply the fix mentioned in the comments of HandleReplacedAllComponentModels.

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