Animated switches

This commit is contained in:
MaddoScientisto 2026-03-01 17:17:04 +01:00
commit f7240e1167
6 changed files with 422 additions and 233 deletions

View file

@ -15,13 +15,13 @@ public partial class Switch3D : Interactable3D
[Export] public ActivationType ActivationType { get; set; } = ActivationType.Toggle;
[Signal]
public delegate void OnActivatedEventHandler(ActivationType activationType);
public delegate void OnActivatedEventHandler(ActivationType activationType, bool success);
private AudioStreamPlayer _activationSound;
private AudioStreamPlayer _denySound;
private readonly string _activationSoundName = "ActivationSound";
private readonly string _denySoundName = "ActivationSound";
private readonly string _denySoundName = "DenySound";
public override void _Ready()
{
@ -60,8 +60,6 @@ public partial class Switch3D : Interactable3D
_denySound?.Play();
return false;
}
EmitSignal(SignalName.OnActivated, (int)activationTypeToUse);
// Compatibility for old single system
bool success = ActivateTarget(Target, activationTypeToUse);
@ -83,13 +81,14 @@ public partial class Switch3D : Interactable3D
_denySound?.Play();
}
EmitSignal(SignalName.OnActivated, (int)activationTypeToUse, result);
return result;
}
private bool ActivateTarget(Node target, ActivationType activationType = ActivationType.Toggle)
{
if (target is not IActivable activable) return false;
activable?.Activate(activationType);
activable.Activate(activationType);
return true;
}

View file

@ -0,0 +1,159 @@
using Godot;
namespace Cirno.Scripts.Interactables;
/// <summary>
/// Listens to the <see cref="Switch3D.OnActivated"/> signal and plays a named animation
/// on an <see cref="AnimationPlayer"/> that reflects the switch's new state.
///
/// Works with any Switch3D subclass (including <see cref="ShootableSwitch3D"/>).
///
/// Usage:
/// 1. Add this node as a child of the Switch3D node.
/// 2. Optionally assign <see cref="TargetSwitch"/> and <see cref="TargetAnimationPlayer"/>
/// in the inspector; if left empty both are discovered automatically from the parent.
/// 3. Set the three animation name exports to match animations in your AnimationPlayer.
/// Leave a name empty to skip playing an animation for that state.
/// </summary>
[Tool]
public partial class SwitchAnimationManager : Node
{
/// <summary>
/// The switch to listen to. Auto-discovered from the parent if left empty.
/// </summary>
[Export] public Switch3D TargetSwitch { get; set; }
/// <summary>
/// The AnimationPlayer to drive. Auto-discovered from the parent if left empty.
/// </summary>
[Export] public AnimationPlayer TargetAnimationPlayer { get; set; }
// --- State-based animation name exports ---
/// <summary>Animation to play when the switch transitions to the enabled (on) state.</summary>
[Export] public StringName AnimationEnabled { get; set; } = "";
/// <summary>Animation to play when the switch transitions to the disabled (off) state.</summary>
[Export] public StringName AnimationDisabled { get; set; } = "";
/// <summary>Animation to play when the switch is destroyed.</summary>
[Export] public StringName AnimationDestroyed { get; set; } = "";
/// <summary>
/// Whether the switch is currently considered enabled (on) or disabled (off).
/// Updated on each successful activation.
/// </summary>
public bool IsEnabled { get; private set; }
/// <summary>
/// When true the switch has been destroyed and will no longer respond to activations.
/// </summary>
public bool IsDestroyed { get; private set; }
public override void _Ready()
{
if (Engine.IsEditorHint()) return;
ResolveReferences();
if (TargetSwitch is null)
{
GD.PushWarning($"[SwitchAnimationManager:{Name}] No Switch3D found. Assign TargetSwitch or place this node as a child of one.");
return;
}
TargetSwitch.OnActivated += OnSwitchActivated;
// Start in the disabled state so the AnimationPlayer is running from the beginning.
PlayAnimation(AnimationDisabled);
}
/// <summary>
/// Fills in <see cref="TargetSwitch"/> and <see cref="TargetAnimationPlayer"/> from the
/// parent node when they have not been assigned explicitly in the inspector.
/// </summary>
private void ResolveReferences()
{
var parent = GetParent();
if (TargetSwitch is null)
TargetSwitch = parent as Switch3D;
if (TargetAnimationPlayer is null)
TargetAnimationPlayer = parent?.FindChild("AnimationPlayer", recursive: true, owned: false) as AnimationPlayer
?? parent?.GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
}
private void OnSwitchActivated(ActivationType activationType, bool success)
{
// Ignore failed activations and permanently destroyed switches.
if (!success || IsDestroyed) return;
UpdateState(activationType);
StringName animationName = IsDestroyed ? AnimationDestroyed
: IsEnabled ? AnimationEnabled
: AnimationDisabled;
PlayAnimation(animationName);
}
/// <summary>
/// Transitions <see cref="IsEnabled"/> (and <see cref="IsDestroyed"/>) according to
/// the activation type that was successfully applied:
/// <list type="bullet">
/// <item><see cref="ActivationType.Enable"/> / <see cref="ActivationType.Open"/> → enabled</item>
/// <item><see cref="ActivationType.Disable"/> / <see cref="ActivationType.Close"/> → disabled</item>
/// <item><see cref="ActivationType.Toggle"/> / <see cref="ActivationType.Use"/> → inverted</item>
/// <item><see cref="ActivationType.Destroy"/> → disabled + destroyed</item>
/// </list>
/// </summary>
private void UpdateState(ActivationType activationType)
{
switch (activationType)
{
case ActivationType.Enable:
case ActivationType.Open:
IsEnabled = true;
break;
case ActivationType.Disable:
case ActivationType.Close:
IsEnabled = false;
break;
case ActivationType.Toggle:
case ActivationType.Use:
IsEnabled = !IsEnabled;
break;
case ActivationType.Destroy:
IsEnabled = false;
IsDestroyed = true;
break;
}
}
/// <summary>
/// Plays the given animation on <see cref="TargetAnimationPlayer"/> if the name is non-empty
/// and the animation exists. Pushes a warning otherwise.
/// </summary>
private void PlayAnimation(StringName animationName)
{
if (animationName is null || animationName == "") return;
if (TargetAnimationPlayer is null)
{
GD.PushWarning($"[SwitchAnimationManager:{Name}] No AnimationPlayer found. Assign TargetAnimationPlayer or add one under the switch.");
return;
}
if (!TargetAnimationPlayer.HasAnimation(animationName))
{
GD.PushWarning($"[SwitchAnimationManager:{Name}] AnimationPlayer has no animation named '{(string)animationName}'.");
return;
}
TargetAnimationPlayer.Play(animationName);
}
}

View file

@ -0,0 +1 @@
uid://cklsqt246d571