mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-02 11:28:19 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
494 lines
13 KiB
C#
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()
|
|
{
|
|
|
|
}
|
|
}
|