using Sandbox.Utility; using System.Runtime.CompilerServices; namespace Sandbox; [Expose, ActionGraphIgnore] [Icon( "control_camera" )] public partial class GameTransform { /// /// Automatically interpolate the transform over multiple frames when changed within the context /// of a fixed update. This results in a smoother appearance for a moving . /// static bool FixedUpdateInterpolation { get; set; } = true; [ActionGraphInclude] public GameObject GameObject { get; } /// /// Are we following our parent object? /// [MethodImpl( MethodImplOptions.AggressiveInlining )] bool IsFollowingParent() { // Is flagged not to follow parent if ( GameObject.Flags.Contains( GameObjectFlags.Absolute ) ) return false; // Has no parent if ( GameObject.Parent is null ) return false; // Parent is a scene if ( GameObject.Parent is Scene && GameObject.Parent is not PrefabScene ) return false; return true; } public TransformProxy Proxy { get; set; } /// /// Returns true if we're inside the transform changed callback. /// internal bool InsideChangeCallback { get; private set; } /// /// Returns true if we're interpolating the transform, which means we're not inside a change callback and /// we're not in a fixed update scene. /// private bool IsInterpolating => !InsideChangeCallback && !(GameObject?.Scene?.IsFixedUpdate ?? false); internal GameTransform( GameObject owner ) { GameObject = owner; _interpolatedLocal = Transform.Zero; _targetLocal = Transform.Zero; _hasPositionSet = false; } bool _hasPositionSet; /// /// The current interpolated local transform. /// [ActionGraphInclude( AutoExpand = true )] public Transform InterpolatedLocal { get { return Proxy?.GetLocalTransform() ?? _interpolatedLocal; } } /// /// The current local transform. /// [ActionGraphInclude( AutoExpand = true )] public Transform Local { get { if ( Proxy is not null ) return Proxy.GetLocalTransform(); if ( !IsInterpolating ) return _targetLocal; return _interpolatedLocal; } set { if ( value == default ) value = Transform.Zero; if ( value.Position.IsNaN ) { Log.Warning( "Ignoring NaN Position" ); return; } if ( !(GameObject?.HasAuthority() ?? true) ) return; if ( Proxy is not null ) { Proxy.SetLocalTransform( value ); return; } var isFixedUpdate = GameObject?.Scene?.IsFixedUpdate ?? false; var isEnabled = GameObject?.Enabled ?? false; var isInterpolationDisabled = GameObject?.Flags.Contains( GameObjectFlags.NoInterpolation ) ?? false; SetLocalTransform( value, FixedUpdateInterpolation && isFixedUpdate && isEnabled && !isInterpolationDisabled ); } } void SetLocalTransform( in Transform value, bool interpolate = false ) { if ( _targetLocal == value ) return; if ( interpolate && _hasPositionSet ) { UpdateInterpolatedLocal( value ); } else { UpdateLocal( value ); } } /// /// Sets the local transform without firing a bunch of "transform changed" callbacks. /// The assumption is that you're changing a bunch of child transforms, and will then call /// transform changed on the root, which will then invoke all the callbacks just once. /// This is what the animation system does! /// internal bool SetLocalTransformFast( in Transform value ) { if ( _targetLocal == value ) return false; _interpolatedLocal = value; _targetLocal = value; return true; } /// /// The target world transform. For internal use only. /// internal Transform TargetWorld { get { if ( Proxy is not null ) return Proxy.GetWorldTransform(); if ( !IsFollowingParent() ) return TargetLocal; return GameObject.Parent.Transform.TargetWorld.ToWorld( TargetLocal ); } } /// /// The world transform gets cached to avoid recalculating it every time. It is invalidated in TransformChanged, which /// is called recursively down children when the transform changes. /// Transform? _worldCached; Transform? _worldInterpCached; /// /// The current world transform. /// public Transform World { get { if ( Proxy is not null ) return Proxy.GetWorldTransform(); if ( !IsFollowingParent() ) return Local; if ( IsInterpolating ) { if ( _worldInterpCached is Transform cached ) return cached; var result = GameObject.Parent.Transform.World.ToWorld( Local ); _worldInterpCached = result; return result; } else { if ( _worldCached is Transform cached ) return cached; var result = GameObject.Parent.Transform.World.ToWorld( Local ); _worldCached = result; return result; } } set { if ( !(GameObject?.HasAuthority() ?? true) ) return; SetWorldInternal( value ); } } /// /// Set from the provided in world-space. /// /// The world-space transform. internal void SetWorldInternal( Transform value ) { if ( value == default ) value = Transform.Zero; if ( value.Position.IsNaN ) { Log.Warning( "Ignoring NaN Position" ); return; } if ( Proxy is not null ) { Proxy.SetWorldTransform( value ); return; } if ( !IsFollowingParent() ) { Local = value; return; } var localTransform = GameObject.Parent.WorldTransform.ToLocal( value ); SetLocalTransform( localTransform ); } /// /// The position in world coordinates. /// [ActionGraphInclude] [Obsolete( "Use WorldPosition instead of Transform.Position" )] public Vector3 Position { get => World.Position; set { if ( value.IsNaN ) throw new ArgumentOutOfRangeException( nameof( value ), @"Position is NaN" ); World = World.WithPosition( value ); } } /// /// The rotation in world coordinates. /// [ActionGraphInclude] [Obsolete( "Use WorldRotation instead of Transform.Rotation" )] public Rotation Rotation { get => World.Rotation; set { World = World.WithRotation( value ); } } /// /// The scale in world coordinates. /// [ActionGraphInclude] [DefaultValue( 1f )] [Obsolete( "Use WorldScale instead of Transform.Scale" )] public Vector3 Scale { get => World.Scale; set => World = World.WithScale( value ); } /// /// Position in local coordinates. /// [ActionGraphInclude] [Property] [Obsolete( "Use LocalPosition instead of Transform.LocalPosition" )] public Vector3 LocalPosition { get => Local.Position; set { Local = Local.WithPosition( value ); } } /// /// Rotation in local coordinates. /// [ActionGraphInclude] [Property] [Obsolete( "Use LocalRotation instead of Transform.LocalRotation" )] public Rotation LocalRotation { get => Local.Rotation; set { Local = Local.WithRotation( value ); } } /// /// Scale in local coordinates. /// [ActionGraphInclude] [Property] [DefaultValue( 1f )] [Obsolete( "Use LocalScale instead of Transform.LocalScale" )] public Vector3 LocalScale { get => Local.Scale; set { Local = Local.WithScale( value ); } } /// /// Performs linear interpolation between this and the given transform. /// /// The destination transform. /// Fraction, where 0 would return this, 0.5 would return a point between this and given transform, and 1 would return the given transform. [ActionGraphInclude] public void LerpTo( in Transform target, float frac ) { var tx = World; tx = Transform.Lerp( tx, target, frac, true ); World = tx; } internal void FromNetwork( Transform transform, bool clearInterpolation ) { if ( GameObject.Network.Interpolation && !clearInterpolation ) { _networkTransformBuffer.Add( new( transform ), Time.Now ); Interpolate = true; } else { if ( clearInterpolation ) Interpolate = false; SetLocalTransform( transform ); } } /// /// Disable the proxy temporarily /// public IDisposable DisableProxy() { if ( Proxy is null ) return default; var saved = Proxy; Proxy = default; return DisposeAction.Create( () => { Proxy = saved; } ); } }