This undocumented behaviour is counter-intuitive and causes expected behaviour.
When calling
SendCustomEventDelayedFrames()
inside
OnDeserialization()
, your event will run in the same frame as the
OnDeserialization()
(as reported by
Time.frameCount
) as opposed to the expected behaviour of being in the next Update().
This effectively causes a "time travel" back in time because the
FixedUpdate()
can be at a later point in real time than the
Update()
, but the
SendCustomEventDelayedFrames()
is "delaying" you back to the frame of
Update()
which was fired in the past.
For testing, put below
WhyDeserializationInFixedUpdate
script onto a cube, click it to trigger a manual sync, and check the logs on the non-owner side.
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class WhyDeserializationInFixedUpdate : UdonSharpBehaviour
{
private int _checkingFrameIndex = 1;
private bool isInFixedUpdate => Time.deltaTime == Time.fixedDeltaTime;
[UdonSynced]
private int _syncedInt;
public override void Interact()
{
if (!Networking.IsOwner(gameObject)) Networking.SetOwner(Networking.LocalPlayer, gameObject);
_syncedInt++;
RequestSerialization();
}
public override void OnDeserialization()
{
Debug.Log($"OnDeserialization, isInFixedUpdate={isInFixedUpdate}");
CheckFrameTimeFor10Frames();
}
public void CheckFrameTimeFor10Frames()
{
Debug.Log($"[TEST] This is a new frame");
Debug.Log(
$"[TEST] F#{_checkingFrameIndex} fCount={Time.frameCount} unscaledTime={Time.unscaledTimeAsDouble:F12} fixedUnscaledTime={Time.fixedUnscaledTimeAsDouble:F12} realtime={Time.realtimeSinceStartupAsDouble:F12} _syncedInt={_syncedInt} isInFixedUpdate={isInFixedUpdate}");
Debug.Log("----------");
_checkingFrameIndex++;
if (_checkingFrameIndex > 10)
{
_checkingFrameIndex = 1;
return;
}
SendCustomEventDelayedFrames(nameof(CheckFrameTimeFor10Frames), 1);
}
}