Files
sbox-public/engine/Sandbox.Engine/Scene/Components/Component.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

494 lines
13 KiB
C#

using Sandbox.Internal;
using Sandbox.Utility;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Sandbox;
/// <summary>
/// A GameObject can have many components, which are the building blocks of the game.
/// </summary>
[Expose, ActionGraphIgnore, ActionGraphExposeWhenCached, Icon( "category" )]
public abstract partial class Component : IJsonConvert, IComponentLister, IValid
{
/// <summary>
/// The scene this Component is in. This is a shortcut for `GameObject.Scene`.
/// </summary>
[ActionGraphInclude]
public Scene Scene => GameObject?.Scene;
/// <summary>
/// The transform of the GameObject this component belongs to. Components don't have their own transforms
/// but they can access the transform of the GameObject they belong to. This is a shortcut for `GameObject.Transform`.
/// </summary>
[ActionGraphInclude]
public GameTransform Transform => GameObject?.Transform;
/// <summary>
/// The GameObject this component belongs to.
/// </summary>
[ActionGraphInclude]
public GameObject GameObject { get; internal set; }
/// <summary>
/// Allow creating tasks that are automatically cancelled when the GameObject is destroyed.
/// </summary>
protected TaskSource Task => GameObject?.Task ?? TaskSource.Cancelled;
/// <summary>
/// Access components on this component's GameObject
/// </summary>
public ComponentList Components => GameObject?.Components;
bool _isInitialized = false;
/// <summary>
/// Called to call Awake, once, at startup.
/// </summary>
internal void InitializeComponent()
{
if ( _isInitialized ) return;
if ( GameObject is null ) return;
if ( !GameObject.Active ) return;
SceneMetrics.ComponentsCreated++;
_isInitialized = true;
if ( !GameObject.Flags.Contains( GameObjectFlags.Deserializing ) )
CheckRequireComponent();
if ( ShouldExecute )
{
CallbackBatch.Add( CommonCallback.Awake, InternalOnAwake, this, "OnAwake" );
}
}
bool _enabledState = false;
bool _enabled = false;
bool _onEnabled = false;
/// <summary>
/// <para>
/// The enable state of this <see cref="Component"/>.
/// </para>
/// <para>
/// This doesn't tell you whether the component is actually active because its parent
/// <see cref="Sandbox.GameObject"/> might be disabled. This merely tells you what the
/// component wants to be. You should use <see cref="Active"/> to determine whether the
/// object is truly active in the scene.
/// </para>
/// </summary>
[ActionGraphInclude]
public bool Enabled
{
get => _enabled;
set
{
if ( _enabled == value ) return;
_enabled = value;
UpdateEnabledStatus();
}
}
/// <summary>
/// True if this Component is enabled, and all of its ancestor GameObjects are enabled
/// </summary>
[ActionGraphInclude]
public bool Active
{
get => _enabledState;
}
public bool IsValid => GameObject is not null && Scene is not null;
/// <summary>
/// Should this component execute? Should OnUpdate, OnEnabled get called?
/// </summary>
private bool ShouldExecute
{
get
{
// PrefabCacheScene don't want to OnEnabled or Update or anything
if ( Scene is PrefabCacheScene ) return false;
// No scene? No execute.
if ( Scene is null ) return false;
// If we're an editor scene, only execute if ExecuteInEditor is enabled
if ( Scene.IsEditor && this is not ExecuteInEditor ) return false;
// If we're a dedicated server, don't execute if DontExecuteOnServer is enabled
if ( Application.IsDedicatedServer && this is DontExecuteOnServer ) return false;
// Maybe Scene.ExecutionEnabled should exist
return true;
}
}
/// <summary>
/// Called once per component
/// </summary>
protected virtual void OnAwake() { }
private void InternalOnAwake()
{
//
// If these trigger, it means they probably did new GameObject or something without an active scene
// which means the gameobject got created either on no scene, or on the wrong scene. This is remedied
// in editor by making sure the correct session is pushed. This is remedied in game by making sure that
// we're in a scope (either menu or game).
//
// These issues should be FIXED. Not HIDDEN. They will cause downstream issues.
//
{
var name = $"{GetType().Name} on ({GameObject?.Name ?? "null"})";
Assert.NotNull( Game.ActiveScene, $"Calling awake on {name} but active scene is null - not {GameObject.Scene}" );
Assert.AreEqual( GameObject.Scene, Game.ActiveScene, $"Calling awake on {name} but active scene is {Game.ActiveScene}, not {GameObject.Scene}" );
}
// Disable any interpolation during OnAwake. We might be created in a Fixed Update context.
using ( GameTransform.DisableInterpolation() )
{
OnAwake();
}
}
internal virtual void OnEnabledInternal()
{
// make sure we only fire this once, and ensure the component is still enabled
if ( _onEnabled || !_enabledState || GameObject == null || GameObject.IsDestroyed )
return;
// Disable any interpolation during OnEnabled. We might be created in a Fixed Update context.
using ( GameTransform.DisableInterpolation() )
{
_onEnabled = true;
OnEnabled();
OnComponentEnabled?.Invoke();
}
}
/// <summary>
/// Called after Awake or whenever the component switches to being enabled (because a gameobject heirachy active change, or the component changed)
/// </summary>
protected virtual void OnEnabled() { }
internal virtual void OnDisabledInternal()
{
// make sure we only fire this once, and ensure the component is still disabled
if ( !_onEnabled || _enabledState )
return;
// Disable any interpolation during OnDisabled.
using ( GameTransform.DisableInterpolation() )
{
_onEnabled = false;
OnDisabled();
OnComponentDisabled?.Invoke();
}
}
internal virtual void OnDestroyInternal()
{
ExceptionWrap( "OnDestroy", OnDestroy );
ExceptionWrap( "OnDestroy", OnComponentDestroy );
// Unlink from GameObject now so we're no longer valid
GameObject = null;
SceneMetrics.ComponentsDestroyed++;
}
protected virtual void OnDisabled() { }
/// <summary>
/// Called once, when the component or gameobject is destroyed
/// </summary>
protected virtual void OnDestroy() { }
/// <summary>
/// When enabled, called every frame, does not get called on a dedicated server
/// </summary>
protected virtual void OnPreRender() { }
internal void OnPreRenderInternal()
{
if ( !ShouldExecute )
return;
ExceptionWrap( "OnPreRender", OnPreRender );
}
private Action _onComponentUpdate;
private Action _onComponentFixedUpdate;
[Group( "Component" )]
[Property]
public Action OnComponentEnabled { get; set; }
[Group( "Component" )]
[Property]
public Action OnComponentStart { get; set; }
[Group( "Component" )]
[Property]
public Action OnComponentUpdate
{
get => _onComponentUpdate;
set => SetUpdateAction<IUpdateSubscriber>( ref _onComponentUpdate, value, Scene.updateComponents );
}
[Group( "Component" )]
[Property]
public Action OnComponentFixedUpdate
{
get => _onComponentFixedUpdate;
set => SetUpdateAction<IFixedUpdateSubscriber>( ref _onComponentFixedUpdate, value, Scene.fixedUpdateComponents );
}
[Group( "Component" )]
[Property]
public Action OnComponentDisabled { get; set; }
[Group( "Component" )]
[Property]
public Action OnComponentDestroy { get; set; }
internal void UpdateEnabledStatus()
{
using var batch = CallbackBatch.Batch();
var state = _enabled && Scene is not null && GameObject is not null && GameObject.Active;
if ( state == _enabledState ) return;
_enabledState = state;
if ( _enabledState )
{
InitializeComponent();
if ( ShouldExecute )
{
CallbackBatch.Add( CommonCallback.Enable, OnEnabledInternal, this, "OnEnabled" );
}
Scene.RegisterComponent( this );
}
else
{
if ( ShouldExecute )
{
CallbackBatch.Add( CommonCallback.Disable, OnDisabledInternal, this, "OnDisabled" );
}
Scene.UnregisterComponent( this );
}
}
/// <summary>
/// Replaces <paramref name="currentAction"/> with <paramref name="newAction"/>, and adds / removes this component
/// from the given <paramref name="updateSet"/>, depending on whether the new action is null, and this type implements
/// the given <typeparamref name="TSubscriber"/> interface.
/// </summary>
private void SetUpdateAction<TSubscriber>( ref Action currentAction, Action newAction, HashSetEx<Component> updateSet )
{
var hadAction = currentAction is not null;
var hasAction = newAction is not null;
currentAction = newAction;
if ( !_enabledState ) return;
if ( this is TSubscriber || hadAction == hasAction ) return;
if ( hasAction )
{
updateSet.Add( this );
}
else
{
updateSet.Remove( this );
}
}
/// <summary>
/// Destroy this component, if it isn't already destroyed. The component will be removed from its
/// GameObject and will stop existing. You should avoid interating with the component after calling this.
/// </summary>
[ActionGraphInclude]
public void Destroy()
{
using var batch = CallbackBatch.Batch();
// already destroyed
if ( !IsValid )
return;
GameObject.Components.OnDestroyedInternal( this );
CallbackBatch.Add( CommonCallback.Destroy, OnDestroyInternal, this, "OnDestroy" );
if ( _enabledState )
{
_enabledState = false;
_enabled = false;
Scene.UnregisterComponent( this );
if ( ShouldExecute )
{
CallbackBatch.Add( CommonCallback.Disable, OnDisabledInternal, this, "OnDisabled" );
}
}
}
/// <summary>
/// Destroy the parent GameObject. This really only exists so when you're typing Destroy you realise
/// that calling Destroy only destroys the Component - not the whole GameObject.
/// </summary>
public void DestroyGameObject() => GameObject?.Destroy();
[ActionGraphInclude]
public virtual void Reset()
{
var t = Game.TypeLibrary.GetType( GetType() );
var so = Game.TypeLibrary.GetSerializedObject( this );
foreach ( var field in t.Fields.Where( x => x.HasAttribute<PropertyAttribute>() ) )
{
var serialized = so.GetProperty( field.Name );
serialized.SetValue( serialized.GetDefault() );
}
foreach ( var prop in t.Properties.Where( x => x.HasAttribute<PropertyAttribute>() ) )
{
var serialized = so.GetProperty( prop.Name );
serialized.SetValue( serialized.GetDefault() );
}
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
void ExceptionWrap( string name, Action a )
{
if ( a is null )
return;
try
{
a();
}
catch ( System.Exception e )
{
Log.Error( e, $"Exception when calling '{name}' on {this}" );
}
}
/// <summary>
/// Called immediately after deserializing, and when a property is changed in the editor.
/// </summary>
protected virtual void OnValidate()
{
}
/// <summary>
/// Called immediately after being refreshed from a network snapshot.
/// </summary>
protected virtual void OnRefresh()
{
}
internal void OnValidateInternal()
{
ExceptionWrap( "OnValidate", OnValidate );
}
internal void Validate()
{
CallbackBatch.Add( CommonCallback.Validate, OnValidateInternal, this, "OnValidate" );
}
internal void OnRefreshInternal()
{
OnRefresh();
}
/// <summary>
/// Called when something on the component has been edited
/// </summary>
[Obsolete( "EditLog is obsolete use Scene.Editor.UndoScope or Scene.Editor.AddUndo instead." )]
public void EditLog( string name, object source )
{
OnValidateInternal();
}
/// <inheritdoc cref="GameObject.Tags"/>
public ITagSet Tags => GameObject.Tags;
/// <summary>
/// When tags have been updated
/// </summary>
protected virtual void OnTagsChanged()
{
}
internal virtual void OnTagsUpdatedInternal()
{
OnTagsChanged();
}
/// <summary>
/// The parent has changed from one parent to another
/// </summary>
protected virtual void OnParentChanged( GameObject oldParent, GameObject newParent )
{
}
internal void OnParentChangedInternal( GameObject oldParent, GameObject newParent )
{
OnParentChanged( oldParent, newParent );
}
/// <summary>
/// Invoke a method in x seconds. Won't be invoked if the component is no longer active.
/// </summary>
public async void Invoke( float secondsDelay, Action action, CancellationToken ct = default )
{
await Task.DelaySeconds( secondsDelay );
if ( !this.IsValid() ) return;
if ( !this.Active ) return;
if ( ct.IsCancellationRequested ) return;
action.InvokeWithWarning();
}
/// <summary>
/// Allows drawing of temporary debug shapes and text in the scene
/// </summary>
public DebugOverlaySystem DebugOverlay => GameObject?.DebugOverlay;
internal void OnParentDestroyInternal()
{
OnParentDestroy();
}
/// <summary>
/// The parent object is being destroyed. This is a nice place to switch to a healthier parent.
/// </summary>
public virtual void OnParentDestroy()
{
}
}