rokojori_action_library/Runtime/Interactions/LockOn/CameraLocker.cs

294 lines
6.6 KiB
C#

using Godot;
using Rokojori.Extensions;
namespace Rokojori
{
// Single owner of lock state. LockOnPointer only tracks candidates and the current selection;
// ThirdPersonCamera (or anything else) only reads isLocked/lockTarget from here.
[Tool]
[GlobalClass]
public partial class CameraLocker : Action
{
[Export]
public LockOnPointer source;
[Export]
public Sensor engageSensor;
[ExportGroup( "Release" )]
[Export]
public float releaseDistance = 0f;
[Export]
public Node3D releaseDistanceOrigin;
[ExportGroup( "Cycling" )]
[Export]
public LockOnCyclingSetup cyclingSetup;
[Export]
public LockOnCyclingSetup lockedCyclingSetup;
[ExportGroup( "Visualizer" )]
[Export]
public Node3D visualizerMarker;
[ExportGroup( "Candidate Events" )]
[Export]
public Action onCandidateSpotted;
[Export]
public Action onCandidateUnlocked;
[Export]
public Action onCandidateVanished;
[Export]
public Action onCandidateChanged;
[Export]
public bool triggerCanidatesWhenLocked = false;
[ExportGroup( "Lock Events" )]
[Export]
public Action onLock;
[Export]
public Action onReleasedLock;
[Export]
public Action onChangeLocked;
[ExportGroup( "Read Only" )]
[Export]
public Pointable lockTarget;
public bool isLocked => lockTarget != null;
Pointable _previousCandidate;
bool _wasEngaged = false;
protected override void _OnTrigger()
{
if ( source == null )
{
return;
}
source.Trigger();
UpdateCandidateEvents();
var previousTarget = lockTarget;
var wasLocked = previousTarget != null;
// Order matters: cycling may move an existing lock, engagement may start/end one,
// auto-release may end one - each only touches lockTarget for its own reason, nothing
// here resyncs it every frame just because source.pointable drifted on its own.
UpdateCycling();
UpdateEngagement();
UpdateAutoRelease();
FireLockEvents( wasLocked, previousTarget );
UpdateVisualizer();
}
public void Release()
{
if ( lockTarget == null )
{
return;
}
var previousTarget = lockTarget;
lockTarget = null;
FireLockEvents( true, previousTarget );
}
void UpdateCandidateEvents()
{
if ( lockTarget != null && ! triggerCanidatesWhenLocked )
{
return;
}
var candidate = source.pointable;
if ( candidate == _previousCandidate )
{
return;
}
_previousCandidate = candidate;
onCandidateChanged?.Trigger();
Action.TriggerSafe( candidate == null ? onCandidateVanished : onCandidateSpotted );
}
void UpdateCycling()
{
var setup = isLocked ? ( lockedCyclingSetup ?? cyclingSetup ) : ( cyclingSetup ?? lockedCyclingSetup );
if ( setup == null )
{
return;
}
var cycled = false;
if ( setup.cycleMode == LockOnCycleMode.ClosestCycle )
{
if ( setup.cycleSensor != null && setup.cycleSensor.isDown )
{
source.CycleNext();
cycled = true;
}
}
else if ( setup.cycleMode == LockOnCycleMode.Directional && setup.directionalCycleInputs != null )
{
for ( int i = 0; i < setup.directionalCycleInputs.Length; i++ )
{
var input = setup.directionalCycleInputs[ i ];
if ( input == null || input.sensor == null || ! input.sensor.isDown )
{
continue;
}
source.CycleInScreenSpaceDirection( input.direction );
cycled = true;
break;
}
}
// Cycling is the only thing that is allowed to move an already-established lock onto a
// different candidate - a deliberate, one-time assignment here, not a per-frame resync.
if ( cycled && isLocked )
{
lockTarget = source.pointable;
}
}
void UpdateEngagement()
{
var engaged = engageSensor != null && engageSensor.isActive;
var risingEdge = engaged && ! _wasEngaged;
_wasEngaged = engaged;
if ( ! engaged )
{
lockTarget = null;
return;
}
// Only a fresh press/toggle-on acquires a target. Once locked, the lock is held
// regardless of what source.pointable does on its own (e.g. losing/regaining the raycast
// hit) - it only changes via cycling, auto-release, or an explicit Release() call.
if ( lockTarget == null && risingEdge )
{
lockTarget = source.pointable;
}
}
void UpdateAutoRelease()
{
if ( lockTarget == null )
{
return;
}
if ( IsGone( lockTarget ) )
{
lockTarget = null;
return;
}
if ( IsTooFar( lockTarget ) )
{
lockTarget = null;
}
}
// Covers both an actually freed node and one that was removed from the tree without being
// freed (e.g. pooled/recycled enemies) - either way it is no longer something the lock
// should keep pointing at.
static bool IsGone( Pointable target )
{
return target == null || ! IsInstanceValid( target ) || ! target.IsInsideTree();
}
bool IsTooFar( Pointable target )
{
if ( releaseDistance <= 0f )
{
return false;
}
var origin = releaseDistanceOrigin ?? source.caster;
if ( origin == null )
{
return false;
}
return origin.GlobalPosition.DistanceTo( target.GlobalPosition ) > releaseDistance;
}
void ReleaseEngageSensorIfToggled()
{
if ( engageSensor is ActivationModeSensor ams && ams.isActive )
{
ams.Release();
}
}
void FireLockEvents( bool wasLocked, Pointable previousTarget )
{
var isLockedNow = lockTarget != null;
if ( isLockedNow && ! wasLocked )
{
Action.TriggerSafe( onLock );
return;
}
if ( ! isLockedNow && wasLocked )
{
ReleaseEngageSensorIfToggled();
Action.TriggerSafe( onReleasedLock );
if ( source.IsCandidate( previousTarget ) )
{
Action.TriggerSafe( onCandidateUnlocked );
}
return;
}
if ( isLockedNow && wasLocked && lockTarget != previousTarget )
{
Action.TriggerSafe( onChangeLocked );
}
}
void UpdateVisualizer()
{
if ( visualizerMarker == null )
{
return;
}
var current = lockTarget ?? source.pointable;
if ( ! IsGone( current ) )
{
visualizerMarker.CopyGlobalPoseFrom( current.pointerPosition );
}
}
}
}