A base Component subclass that integrates with Sandbox.Reactivity. It manages an effect root lifecycle, provides activation timing (OnEnabled or OnStart), exposes reactive property container producers, and helper methods to send and listen for GameObject events via effects.
#if SANDBOX
using System.Diagnostics;
using Sandbox.Reactivity.Internals;
using static Sandbox.Reactivity.Reactive;
#if JETBRAINS_ANNOTATIONS
#endif
namespace Sandbox.Reactivity;
/// <summary>
/// The base component used to opt into the reactivity system. This allows usage of the reactive property attributes,
/// and automatically creates an effect root in <see cref="OnActivate" />.
/// </summary>
/// <seealso cref="ReactiveAttribute" />
/// <seealso cref="DerivedAttribute" />
#if JETBRAINS_ANNOTATIONS
#endif
public abstract class ReactiveComponent(ReactiveComponent.ActivationStage activationStage) : Component,
IReactivePropertyContainer
{
/// <summary>
/// Describes at what point during its lifecycle a <see cref="ReactiveComponent" /> will run its activation method.
/// </summary>
public enum ActivationStage
{
/// <summary>
/// Calls <see cref="ReactiveComponent.OnActivate" /> during <see cref="Component.OnEnabled" />. This is the
/// default.
/// </summary>
OnEnabled,
/// <summary>
/// Calls <see cref="ReactiveComponent.OnActivate" /> during <see cref="Component.OnStart" />. Useful for when
/// you want to access data on other components on the same game object that is only available <i>after</i>
/// they're enabled.
/// </summary>
/// <remarks>
/// If the component is disabled and re-enabled, the activation will subsequently run during
/// <see cref="Component.OnEnabled" /> since <see cref="Component.OnStart" /> is only called once ever.
/// </remarks>
OnStart,
}
/// <summary>
/// When to run this component's activation method.
/// </summary>
private readonly ActivationStage _activationStage = activationStage;
/// <summary>
/// The disposable for this component's effect root that exists while it's enabled.
/// </summary>
private Effect? _effectRoot;
/// <summary>
/// Whether this component has ever called <see cref="Component.OnStart" />.
/// </summary>
private bool _hasStarted;
protected ReactiveComponent()
: this(ActivationStage.OnEnabled)
{
}
Dictionary<int, IProducer> IReactivePropertyContainer.Producers { get; } = [];
/// <inheritdoc cref="GameObjectExtensions.SendDirect" />
public void SendDirect<T>(T eventData)
{
GameObject?.SendDirect(eventData);
}
/// <inheritdoc cref="GameObjectExtensions.SendUp" />
public void SendUp<T>(T eventData)
{
GameObject?.SendUp(eventData);
}
/// <inheritdoc cref="GameObjectExtensions.SendDown" />
public void SendDown<T>(T eventData)
{
GameObject?.SendDown(eventData);
}
/// <summary>
/// Runs a function when this component's game object receives an event.
/// </summary>
/// <param name="callback">
/// The function to run when the event is received. If the function returns <c>true</c>, the event will prevent
/// further propagation to ancestor/descendant game objects.
/// </param>
/// <typeparam name="T">
/// The type of event to listen to. Any received game objects that are assignable to this type will run the
/// function.
/// </typeparam>
public void OnEvent<T>(Func<T, bool> callback)
{
if (GameObject is not { } go)
{
return;
}
var manager = GameObjectEventManager.GetOrCreate(go);
var parent = Runtime.EnsureCurrentEffect();
var effect = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
{
manager.Add(callback);
return () => manager.Remove(callback);
},
parent,
false);
effect.SetDebugInfo($"OnEvent<{typeof(T).ToSimpleString(false)}>",
"electric_bolt",
new CallLocation(1),
parent);
effect.Run();
}
[StackTraceHidden]
private void CreateRootEffect()
{
_effectRoot?.Dispose();
// component lifetimes are managed by their owning game objects, it doesn't make sense to bind it to the
// lifetime of the currently executing effect
_effectRoot = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
{
OnActivate();
return null;
},
null,
false);
_effectRoot.SetDebugInfo(DisplayInfo.For(this).Name,
DisplayInfo.For(this).Icon,
new CallLocation(GetType(), nameof(OnActivate)),
this);
_effectRoot.Run();
}
protected sealed override void OnEnabled()
{
if (_activationStage != ActivationStage.OnStart || _hasStarted)
{
CreateRootEffect();
}
}
protected sealed override void OnStart()
{
if (_activationStage == ActivationStage.OnStart)
{
_hasStarted = true;
CreateRootEffect();
}
}
protected sealed override void OnDisabled()
{
_effectRoot?.Dispose();
_effectRoot = null;
}
/// <summary>
/// Called inside an effect root when this component is enabled, allowing for effects to be created. When this
/// component is disabled, the effect root (and all of its descendants) are disposed.
/// </summary>
protected virtual void OnActivate()
{
}
}
#endif