using System;
using System.Collections.Generic;
using Fusion.Analyzer;
using UnityEngine;
namespace Fusion.Addons.Physics {
///
/// Base class for and ;
///
public abstract class RunnerSimulatePhysicsBase: SimulationBehaviour, IBeforeTick {
///
/// Stored original Physics setting auto-simulate setting, used to restore Unity settings when Fusion runners are shutdown.
///
[StaticField(StaticFieldResetMode.None)]
protected static PhysicsTimings _physicsAutoSimRestore;
///
/// Tracked number of started NetworkRunners. Used to determine when last Runner has stopped,
/// and original Unity physics settings should be restored.
///
[StaticField(StaticFieldResetMode.None)]
private static int _enabledRunnersCount;
// Inspector logic (Used by our WarnIf and DrawIf attributes)
///
/// Used by Fusion inspector UI.
///
internal bool ShowForwardOnly => _physicsTiming == PhysicsTimings.FixedUpdateNetwork;
///
/// Used by Fusion inspector UI.
///
internal bool ShowMultiplier => _physicsAuthority != PhysicsAuthorities.Unity;
///
/// Used by Fusion inspector UI.
///
internal bool WarnAutoSyncTransforms => AutoSyncTransforms && _physicsAuthority != PhysicsAuthorities.Unity && _physicsTiming != PhysicsTimings.Update;
///
/// Indicates whether Unity or Fusion should handle Physics.Simulate() calls.
/// When set to Auto (default), this will pick the most appropriate setting for the Game Mode and Peer Mode settings.
///
[InlineHelp]
[SerializeField]
protected PhysicsAuthorities _physicsAuthority = PhysicsAuthorities.Fusion;
///
/// Public getter of the value.
/// Indicates whether Unity or Fusion should handle Physics.Simulate() calls.
/// When set to Auto (default), this will pick the most appropriate setting for the Game Mode and Peer Mode settings.
///
public PhysicsAuthorities PhysicsAuthority => _physicsAuthority;
///
/// Indicates which timing segment should be used for calling Physics.Simulate().
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(_physicsAuthority), (long)PhysicsAuthorities.Unity, CompareOperator.NotEqual, Hide = true)]
[WarnIf(nameof(WarnAutoSyncTransforms),
"AutoSyncTransforms is enabled in Unity's Project Settings.\n\n" +
"This is potentially costly due to interpolation moving the Rigidbody transform every Update(). " +
"If you have NetworkRigidbody instances which do not have InterpolationTarget set, then it may be preferable to disable AutoSyncTransforms " +
"and manually call SyncTransforms() before Raycast/Overlap queries.",
AsBox = true
)]
protected PhysicsTimings _physicsTiming = PhysicsTimings.FixedUpdateNetwork;
///
/// Public getter of the value.
/// Indicates which timing segment should be used for calling Physics.Simulate().
///
public PhysicsTimings PhysicsTiming => _physicsTiming;
///
/// If enabled, clients will only call Physics.Simulate() on forward Ticks (a Tick which is being simulated for the first time),
/// and will not simulate physics for re-sims. In nearly all use cases this should be set to false.
/// Only enable this if you know that NO physics prediction exists in your game (all Rigidbody movement is client authority).
/// Not applicable to Shared Mode, as there are no re-simulation nor prediction in that mode and all simulations are forward.
///
[InlineHelp]
[DrawIf(nameof(ShowForwardOnly), Hide = true)]
[SerializeField]
public bool ForwardOnly = false;
///
/// This value is used to scale PhysicsSimulationDeltaTime, typically to speed up and slow down the passing of time.
/// This doesn't change the Fusion TickRate (that value is fixed and cannot be changed once a game is started),
/// and instead changes how much time is simulated each Physics tick. When changing this value, be sure to account for it
/// in all code where you use , as you likely ill want to apply the same modifier everywhere.
///
/// For Physics.Simulate(deltaTime) - the deltaTime is calculated as PhysicsSimulationDeltaTime * DeltaTimeMultiplier.
/// The resulting deltaTime must be a greater than zero value (You cannot simulate using zero or negative values).
/// Values less than zero will be clamped to zero. Default is 1.
/// A value of zero will result in Physics.Simulate not being called at all.
///
[InlineHelp]
[DrawIf(nameof(ShowMultiplier), Hide = true)]
[DisplayName("DeltaTime Multiplier")]
[SerializeField]
public float DeltaTimeMultiplier = 1;
///
/// Sets Time.fixedDeltaTime to match Fusion.DeltaTime, ensuring that Unity is calling FixedUpdate
/// at approx. the same interval that Fusion is calling FixedUpdateNetwork() forward Ticks
///
[InlineHelp]
[SerializeField]
public bool SetUnityFixedTimestep = false;
///
/// DeltaTime used in FixedUpdateNetwork for Physics.Simulate(deltaTime).
/// By default, returns .
/// Override this if you want to control how much time passes in each tick (for bullet-time or time compression effects).
/// You typically can just set the instead to speed up or slow down time.
///
/// For Physics.Simulate(deltaTime) - the deltaTime is calculated as PhysicsSimulationDeltaTime * DeltaTimeMultiplier.
/// The resulting deltaTime must be a greater than zero value (You cannot simulate using zero or negative values).
/// Values less than zero will be clamped to zero. Default is 1.
/// A value of zero will result in Physics.Simulate not being called at all.
///
public virtual float PhysicsSimulationDeltaTime {
get => Runner.DeltaTime;
}
///
/// Abstracted get/set for Unity's Physics auto-sync transforms setting, for the applicable 3d/2d physics.
///
protected abstract bool AutoSyncTransforms { get; set; }
///
/// Abstracted getter for Unity's Physics physics mode setting, for the applicable 3d/2d physics.
///
protected abstract PhysicsTimings UnityPhysicsMode { get; }
///
/// Sets the auto-simulate setting for the associated Physics engine.
/// If the setting is not currently overridden, the current value of the setting for the physics engine is recorded
/// to allow for restoration later with the method.
///
protected abstract void OverrideAutoSimulate(bool enabled);
///
/// Restore sauto-simulate setting of the associated physics engine to its original value prior to any method calls.
///
protected abstract void RestoreAutoSimulate();
#region Simulation Callbacks
///
/// Callback invoked prior to Simulate() being called.
///
public event Action OnBeforeSimulate;
///
/// Callback invoked prior to Simulate() being called.
///
public event Action OnAfterSimulate;
// One-time callbacks
private readonly Queue _onAfterSimulateCallbacks = new Queue();
private readonly Queue _onBeforeSimulateCallbacks = new Queue();
///
/// Returns true FixedUpdateNetwork has executed for the current tick, and physics has simulated.
///
public bool HasSimulatedThisTick { get; private set; }
///
/// Register a one time callback which will be called immediately before the next physics simulation occurs.
/// Use to determine if simulation has already happened.
///
public void QueueBeforeSimulationCallback(Action callback) {
_onBeforeSimulateCallbacks.Enqueue(callback);
}
///
/// Register a one time callback which will be called immediately after the next physics simulation occurs.
/// Use to determine if simulation has already happened.
///
public void QueueAfterSimulationCallback(Action callback) {
_onAfterSimulateCallbacks.Enqueue(callback);
}
#endregion
///
/// Method which calls simulate() for the associated Unity physics engine,
/// for the primary physics scene of the associated .
///
protected abstract void SimulatePrimaryScene( float deltaTime);
///
/// Method which calls simulate() for the associated Unity physics engine,
/// for any additional physics scenes of the associated .
///
protected abstract void SimulateAdditionalScenes(float deltaTime, bool forwardOnly);
#if UNITY_EDITOR
private void OnValidate() {
if (_physicsTiming == PhysicsTimings.FixedUpdateNetwork && _physicsAuthority == PhysicsAuthorities.Unity) {
Debug.LogWarning($"Unity cannot auto-simulate FixedUpdateNetwork(). Changing {nameof(_physicsAuthority)} to {PhysicsAuthorities.Auto}.");
_physicsAuthority = PhysicsAuthorities.Auto;
}
}
#endif
private bool _isInitialized;
///
/// Initialization code that is run on the first execution of .
///
protected virtual void Startup() {
// Resolve 'Auto" to give Unity or Fusion control of Physics.Simulate
// Should let Unity handle Physics if running Single-Peer, and in a valid Timing that Unity can Handle.
_physicsAuthority = _physicsAuthority == PhysicsAuthorities.Auto ?
Runner.Config.PeerMode == NetworkProjectConfig.PeerModes.Single && (Runner.GameMode == GameMode.Shared || Runner.Mode == SimulationModes.Host) && _physicsTiming != PhysicsTimings.FixedUpdateNetwork ? PhysicsAuthorities.Unity : PhysicsAuthorities.Fusion :
_physicsAuthority;
#if UNITY_EDITOR
if (_physicsAuthority == PhysicsAuthorities.Unity && Runner.Config.PeerMode == NetworkProjectConfig.PeerModes.Multiple) {
Debug.LogWarning($"{GetType().Name}.{nameof(_physicsAuthority)} setting is forcing Unity as the Physics Authority. However in Multi-Peer Mode your Physics Scenes will not simulate. Set to Auto.");
}
#endif
// When the first Runner becomes active, determine if Unity or Fusion should be Simulating Physics, and cache the previous setting for shutdown restore
if (++_enabledRunnersCount == 1) {
OverrideAutoSimulate(_physicsAuthority == PhysicsAuthorities.Unity);
}
// If we ended up letting Unity run physics, make sure FixedUpdate's interval matches Fusion's
if (SetUnityFixedTimestep) {
Time.fixedDeltaTime = Runner.DeltaTime;
}
_isInitialized = true;
}
///
/// Shutdown code executed when associated shuts down.
///
protected virtual void Shutdown() {
// When the last Runner shuts down, restore Physics.AutoSimulate
if (PhysicsAuthority == PhysicsAuthorities.Fusion && --_enabledRunnersCount == 0) {
RestoreAutoSimulate();
}
}
private void Update() {
if (_isInitialized == false) { return; }
// If selected timing is not Update, this Update callback should be ignored
if (_physicsTiming != PhysicsTimings.Update) { return; }
// if Unity is currently auto-simulating - Fusion should not.
if (UnityPhysicsMode != PhysicsTimings.Script) { return; }
var deltaTime = Time.deltaTime * DeltaTimeMultiplier;
// Debug.LogWarning($"Update Sim {deltaTime}");
SimulationExecute(deltaTime, true);
}
///
/// Unity FixedUpdate callback.
///
public void FixedUpdate() {
if (_isInitialized == false) { return; }
if (_physicsTiming == PhysicsTimings.FixedUpdate) {
// For some reason this needs to be reapplied.
if (SetUnityFixedTimestep) {
Time.fixedDeltaTime = Runner.DeltaTime;
}
} else {
// The selected timing is not FixedUpdate, this FixedUpdate callback should be ignored
return;
}
// if Unity is currently auto-simulating - Fusion should not.
if (UnityPhysicsMode != PhysicsTimings.Script) { return; }
var deltaTime = Time.fixedDeltaTime * DeltaTimeMultiplier;
SimulationExecute(deltaTime, true);
}
///
public override void FixedUpdateNetwork() {
// We have no Spawned(), so initializing on first FUN
if (_isInitialized == false) {
Startup();
}
// We have no Despawned(), so testing for shutdown here.
if (Runner.IsShutdown) {
Shutdown();
return;
}
// Currently getting physics info for both Shared and Server Modes
if (Runner.TryGetPhysicsInfo(out NetworkPhysicsInfo info)) {
if (Runner.IsServer || Runner.IsSharedModeMasterClient) {
info.TimeScale = DeltaTimeMultiplier;
Runner.TrySetPhysicsInfo(info);
} else {
DeltaTimeMultiplier = info.TimeScale;
}
}
// If selected timing is not FixedUpdateNetwork, this FixedUpdateNetwork callback should be ignored
if (_physicsTiming != PhysicsTimings.FixedUpdateNetwork) { return; }
// if Unity is currently auto-simulating - Fusion should not.
if (UnityPhysicsMode != PhysicsTimings.Script) { return; }
var deltaTime = PhysicsSimulationDeltaTime * DeltaTimeMultiplier;
bool isForward = Runner.IsForward;
SimulationExecute(deltaTime, isForward);
}
private void SimulationExecute(float deltaTime, bool isForward) {
if (DeltaTimeMultiplier <= 0) {
return;
}
if (isForward || !ForwardOnly) {
DoSimulatePrimaryScene(deltaTime);
}
SimulateAdditionalScenes(deltaTime, isForward);
}
void IBeforeTick.BeforeTick() {
HasSimulatedThisTick = false;
}
///
/// Executes the simulation of primary and secondary physics scenes, and triggers the associated callback interfaces.
///
protected virtual void DoSimulatePrimaryScene(float deltaTime) {
while (_onBeforeSimulateCallbacks.Count > 0) {
_onBeforeSimulateCallbacks.Dequeue().Invoke();
}
OnBeforeSimulate?.Invoke();
SimulatePrimaryScene(deltaTime);
HasSimulatedThisTick = true;
while (_onAfterSimulateCallbacks.Count > 0) {
_onAfterSimulateCallbacks.Dequeue().Invoke();
}
OnAfterSimulate?.Invoke();
}
}
}