using Sandbox.Internal;
namespace SceneTests.Components;
///
/// Tests for the update dispatch in Component.Update.cs. OnUpdate/OnFixedUpdate
/// overrides only run when the component also implements the matching
/// IUpdateSubscriber / IFixedUpdateSubscriber marker (codegen adds these
/// automatically in game code; here we implement them by hand). Plain
/// components can still take part in the update loop through the
/// OnComponentUpdate / OnComponentFixedUpdate action properties.
///
[TestClass]
[DoNotParallelize]
public class ComponentUpdateTest : SceneTest
{
///
/// A component implementing IUpdateSubscriber gets exactly one OnUpdate call
/// per game tick.
///
[TestMethod]
public void OnUpdateRunsOncePerGameTick()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
Assert.AreEqual( 0, comp.UpdateCalls );
scene.GameTick();
Assert.AreEqual( 1, comp.UpdateCalls );
scene.GameTick();
Assert.AreEqual( 2, comp.UpdateCalls );
}
///
/// OnStart runs exactly once, and always before the first OnUpdate.
///
[TestMethod]
public void OnStartRunsOnceBeforeFirstUpdate()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
scene.GameTick();
scene.GameTick();
Assert.AreEqual( 1, comp.StartCalls );
Assert.IsTrue( comp.StartedBeforeFirstUpdate );
}
///
/// Disabling a component stops its OnUpdate from being called; re-enabling
/// it resumes updates.
///
[TestMethod]
public void OnUpdateStopsWhenDisabled()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
scene.GameTick();
Assert.AreEqual( 1, comp.UpdateCalls );
comp.Enabled = false;
scene.GameTick();
Assert.AreEqual( 1, comp.UpdateCalls );
comp.Enabled = true;
scene.GameTick();
Assert.AreEqual( 2, comp.UpdateCalls );
}
///
/// A component implementing IFixedUpdateSubscriber gets OnFixedUpdate calls
/// during the game tick, and stops getting them while disabled.
///
[TestMethod]
public void OnFixedUpdateRunsDuringGameTick()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
Assert.AreEqual( 0, comp.FixedUpdateCalls );
scene.GameTick();
Assert.IsTrue( comp.FixedUpdateCalls > 0 );
var countWhenDisabled = comp.FixedUpdateCalls;
comp.Enabled = false;
scene.GameTick();
Assert.AreEqual( countWhenDisabled, comp.FixedUpdateCalls );
}
///
/// Assigning OnComponentUpdate on a plain component (no IUpdateSubscriber)
/// subscribes it to the update loop; clearing the action unsubscribes it.
///
[TestMethod]
public void OnComponentUpdateActionSubscribesPlainComponent()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
var calls = 0;
comp.OnComponentUpdate = () => calls++;
scene.GameTick();
Assert.AreEqual( 1, calls );
comp.OnComponentUpdate = null;
scene.GameTick();
Assert.AreEqual( 1, calls );
}
///
/// An OnComponentUpdate action assigned while the component is disabled
/// doesn't run, but starts running once the component is enabled.
///
[TestMethod]
public void OnComponentUpdateActionSetWhileDisabled()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create( false );
var calls = 0;
comp.OnComponentUpdate = () => calls++;
scene.GameTick();
Assert.AreEqual( 0, calls );
comp.Enabled = true;
scene.GameTick();
Assert.AreEqual( 1, calls );
}
///
/// The OnComponentStart action runs once on the first tick after the
/// component becomes active, and never again.
///
[TestMethod]
public void OnComponentStartActionRunsOnce()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
var calls = 0;
comp.OnComponentStart = () => calls++;
scene.GameTick();
Assert.AreEqual( 1, calls );
scene.GameTick();
Assert.AreEqual( 1, calls );
}
///
/// Assigning OnComponentFixedUpdate on a plain component subscribes it to
/// the fixed update loop; clearing the action unsubscribes it.
///
[TestMethod]
public void OnComponentFixedUpdateActionSubscribesPlainComponent()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
var calls = 0;
comp.OnComponentFixedUpdate = () => calls++;
scene.GameTick();
Assert.IsTrue( calls > 0 );
var countWhenCleared = calls;
comp.OnComponentFixedUpdate = null;
scene.GameTick();
Assert.AreEqual( countWhenCleared, calls );
}
}
///
/// Component subscribed to per-frame updates via the IUpdateSubscriber marker,
/// recording OnStart/OnUpdate calls and their relative order.
///
public class UpdateProbeComponent : Component, IUpdateSubscriber
{
public int StartCalls;
public int UpdateCalls;
public bool StartedBeforeFirstUpdate;
///
/// Counts OnStart invocations.
///
protected override void OnStart()
{
StartCalls++;
}
///
/// Counts OnUpdate invocations and records whether OnStart ran first.
///
protected override void OnUpdate()
{
if ( UpdateCalls == 0 )
{
StartedBeforeFirstUpdate = StartCalls == 1;
}
UpdateCalls++;
}
}
///
/// Component subscribed to fixed updates via the IFixedUpdateSubscriber marker.
///
public class FixedUpdateProbeComponent : Component, IFixedUpdateSubscriber
{
public int FixedUpdateCalls;
///
/// Counts OnFixedUpdate invocations.
///
protected override void OnFixedUpdate()
{
FixedUpdateCalls++;
}
}
///
/// Component with no update overrides and no subscriber markers - it only
/// takes part in the update loop via the OnComponent* action properties.
///
public class PlainTickComponent : Component
{
}