using Sandbox.Internal; using Sandbox.Utility; using System.Runtime.CompilerServices; using System.Threading; namespace Sandbox; /// /// A GameObject can have many components, which are the building blocks of the game. /// [Expose, ActionGraphIgnore, ActionGraphExposeWhenCached, Icon( "category" )] public abstract partial class Component : IJsonConvert, IComponentLister, IValid { /// /// The scene this Component is in. This is a shortcut for `GameObject.Scene`. /// [ActionGraphInclude] public Scene Scene => GameObject?.Scene; /// /// 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`. /// [ActionGraphInclude] public GameTransform Transform => GameObject?.Transform; /// /// The GameObject this component belongs to. /// [ActionGraphInclude] public GameObject GameObject { get; internal set; } /// /// Allow creating tasks that are automatically cancelled when the GameObject is destroyed. /// protected TaskSource Task => GameObject?.Task ?? TaskSource.Cancelled; /// /// Access components on this component's GameObject /// public ComponentList Components => GameObject?.Components; bool _isInitialized = false; /// /// Called to call Awake, once, at startup. /// 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; /// /// /// The enable state of this . /// /// /// This doesn't tell you whether the component is actually active because its parent /// might be disabled. This merely tells you what the /// component wants to be. You should use to determine whether the /// object is truly active in the scene. /// /// [ActionGraphInclude] public bool Enabled { get => _enabled; set { if ( _enabled == value ) return; _enabled = value; UpdateEnabledStatus(); } } /// /// True if this Component is enabled, and all of its ancestor GameObjects are enabled /// [ActionGraphInclude] public bool Active { get => _enabledState; } public bool IsValid => GameObject is not null && Scene is not null; /// /// Should this component execute? Should OnUpdate, OnEnabled get called? /// 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; } } /// /// Called once per component /// 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(); } } /// /// Called after Awake or whenever the component switches to being enabled (because a gameobject heirachy active change, or the component changed) /// 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() { } /// /// Called once, when the component or gameobject is destroyed /// protected virtual void OnDestroy() { } /// /// When enabled, called every frame, does not get called on a dedicated server /// 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( ref _onComponentUpdate, value, Scene.updateComponents ); } [Group( "Component" )] [Property] public Action OnComponentFixedUpdate { get => _onComponentFixedUpdate; set => SetUpdateAction( 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 ); } } /// /// Replaces with , and adds / removes this component /// from the given , depending on whether the new action is null, and this type implements /// the given interface. /// private void SetUpdateAction( ref Action currentAction, Action newAction, HashSetEx 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 ); } } /// /// 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. /// [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" ); } } } /// /// 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. /// 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() ) ) { var serialized = so.GetProperty( field.Name ); serialized.SetValue( serialized.GetDefault() ); } foreach ( var prop in t.Properties.Where( x => x.HasAttribute() ) ) { 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}" ); } } /// /// Called immediately after deserializing, and when a property is changed in the editor. /// protected virtual void OnValidate() { } /// /// Called immediately after being refreshed from a network snapshot. /// protected virtual void OnRefresh() { } internal void OnValidateInternal() { ExceptionWrap( "OnValidate", OnValidate ); } internal void Validate() { CallbackBatch.Add( CommonCallback.Validate, OnValidateInternal, this, "OnValidate" ); } internal void OnRefreshInternal() { OnRefresh(); } /// /// Called when something on the component has been edited /// [Obsolete( "EditLog is obsolete use Scene.Editor.UndoScope or Scene.Editor.AddUndo instead." )] public void EditLog( string name, object source ) { OnValidateInternal(); } /// public ITagSet Tags => GameObject.Tags; /// /// When tags have been updated /// protected virtual void OnTagsChanged() { } internal virtual void OnTagsUpdatedInternal() { OnTagsChanged(); } /// /// The parent has changed from one parent to another /// protected virtual void OnParentChanged( GameObject oldParent, GameObject newParent ) { } internal void OnParentChangedInternal( GameObject oldParent, GameObject newParent ) { OnParentChanged( oldParent, newParent ); } /// /// Invoke a method in x seconds. Won't be invoked if the component is no longer active. /// 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(); } /// /// Allows drawing of temporary debug shapes and text in the scene /// public DebugOverlaySystem DebugOverlay => GameObject?.DebugOverlay; internal void OnParentDestroyInternal() { OnParentDestroy(); } /// /// The parent object is being destroyed. This is a nice place to switch to a healthier parent. /// public virtual void OnParentDestroy() { } }