2025-05-07 11:36:03 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using Cirno.Scripts.Utils;
|
|
|
|
|
|
using Godot;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Cirno.Scripts.Misc;
|
|
|
|
|
|
|
|
|
|
|
|
public partial class CameraController : Camera2D
|
|
|
|
|
|
{
|
2025-06-13 17:46:44 +02:00
|
|
|
|
public static CameraController Instance { get; private set; }
|
2025-05-07 11:36:03 +02:00
|
|
|
|
[Export] public bool PixelSnap { get; set; } = true;
|
|
|
|
|
|
[Export] public bool EnableSmoothing { get; set; } = true;
|
|
|
|
|
|
|
|
|
|
|
|
[Export] public bool FollowTargeting { get; set; } = true;
|
|
|
|
|
|
|
|
|
|
|
|
[Export] public float SmoothTime { get; set; } = 0.2f;
|
|
|
|
|
|
|
|
|
|
|
|
[Export] public float MaxAimOffsetDistance { get; set; } = 64f;
|
2025-05-07 11:58:08 +02:00
|
|
|
|
[Export] public float AimLerpSpeed { get; set; } = 0.8f; // How fast the offset adapts
|
2025-05-07 11:36:03 +02:00
|
|
|
|
[Export] public float AimDeadzone { get; set; } = 0.2f;
|
|
|
|
|
|
|
|
|
|
|
|
[ExportGroup("Name Strings")]
|
|
|
|
|
|
[Export] public StringName CameraControllersGroupName { get; private set; } = "camera_controllers";
|
|
|
|
|
|
[Export] public StringName DebugCamera1ActionName { get; private set; } = "debug_camera_1";
|
|
|
|
|
|
[Export] public StringName DebugCamera2ActionName { get; private set; } = "debug_camera_2";
|
|
|
|
|
|
|
|
|
|
|
|
[Export] public StringName AimUpName { get; private set; } = "aim_up";
|
|
|
|
|
|
[Export] public StringName AimDownName { get; private set; } = "aim_down";
|
|
|
|
|
|
[Export] public StringName AimLeftName { get; private set; } = "aim_left";
|
|
|
|
|
|
[Export] public StringName AimRightName { get; private set; } = "aim_right";
|
|
|
|
|
|
|
|
|
|
|
|
private CameraTarget _activeTarget;
|
|
|
|
|
|
|
|
|
|
|
|
private Vector2 _previousPixelSnapDelta = Vector2.Zero;
|
|
|
|
|
|
|
|
|
|
|
|
// The current camera velocity, for smooth damping.
|
|
|
|
|
|
private Vector2 _currentVelocity = Vector2.Zero;
|
|
|
|
|
|
|
|
|
|
|
|
// The current exact position of the camera
|
|
|
|
|
|
private Vector2 _currentPosition;
|
|
|
|
|
|
|
|
|
|
|
|
private Vector2 _currentAimOffset = Vector2.Zero;
|
|
|
|
|
|
|
|
|
|
|
|
public override void _Ready()
|
|
|
|
|
|
{
|
2025-06-13 17:46:44 +02:00
|
|
|
|
Instance = this;
|
2025-05-07 11:36:03 +02:00
|
|
|
|
AddToGroup(CameraControllersGroupName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void _UnhandledInput(InputEvent @event)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Input.IsActionJustPressed(DebugCamera1ActionName))
|
|
|
|
|
|
{
|
|
|
|
|
|
PixelSnap = !PixelSnap;
|
|
|
|
|
|
GD.Print($"Camera Pixel Snap: {PixelSnap}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Input.IsActionJustPressed(DebugCamera2ActionName))
|
|
|
|
|
|
{
|
|
|
|
|
|
EnableSmoothing = !EnableSmoothing;
|
|
|
|
|
|
GD.Print($"Camera Smoothing: {EnableSmoothing}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void _Process(double delta)
|
|
|
|
|
|
{
|
|
|
|
|
|
float deltaTime = (float)delta;
|
|
|
|
|
|
// Update position
|
|
|
|
|
|
Vector2 nextPosition;
|
|
|
|
|
|
|
|
|
|
|
|
Vector2 target;
|
|
|
|
|
|
if (FollowTargeting)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Adding the offset
|
|
|
|
|
|
Vector2 aimDirection = GetAimDirection(); // We'll define this next
|
|
|
|
|
|
Vector2 desiredOffset = aimDirection * MaxAimOffsetDistance;
|
|
|
|
|
|
_currentAimOffset = _currentAimOffset.Lerp(desiredOffset, AimLerpSpeed * deltaTime);
|
|
|
|
|
|
|
|
|
|
|
|
// Clamp final offset
|
|
|
|
|
|
if (_currentAimOffset.LengthSquared() > MaxAimOffsetDistance * MaxAimOffsetDistance)
|
|
|
|
|
|
_currentAimOffset = _currentAimOffset.Normalized() * MaxAimOffsetDistance;
|
|
|
|
|
|
|
|
|
|
|
|
target = _activeTarget.GlobalPosition + _currentAimOffset;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
target = _activeTarget.GlobalPosition;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (EnableSmoothing)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Handle target movement with SmoothDamp
|
|
|
|
|
|
// Replace this with any smooth follow / lerp of your choosing, but be careful to use `delta`
|
|
|
|
|
|
// properly -- improper use can result in jitter. Don't do lerp(current, target, delta).
|
|
|
|
|
|
var resX = MathFunctions.SmoothDamp(_currentPosition.X, target.X, _currentVelocity.X, SmoothTime,
|
|
|
|
|
|
float.PositiveInfinity, deltaTime);
|
|
|
|
|
|
var resY = MathFunctions.SmoothDamp(_currentPosition.Y, target.Y, _currentVelocity.Y, SmoothTime,
|
|
|
|
|
|
float.PositiveInfinity, deltaTime);
|
|
|
|
|
|
|
|
|
|
|
|
nextPosition = new Vector2(resX.Item1, resY.Item1);
|
|
|
|
|
|
_currentVelocity = new Vector2(resX.Item2, resY.Item2);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Set next camera position to the exact target.
|
|
|
|
|
|
nextPosition = target;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_currentPosition = nextPosition;
|
|
|
|
|
|
|
|
|
|
|
|
if (PixelSnap)
|
|
|
|
|
|
{
|
|
|
|
|
|
// IMPORTANT: perform pixel snap so that the camera movement doesn't interfere with the
|
|
|
|
|
|
// sub-pixel "smooth movement" we do in the shader.
|
|
|
|
|
|
var snappedPosition = (nextPosition + new Vector2(0.5f, 0.5f)).Floor();
|
|
|
|
|
|
_previousPixelSnapDelta = snappedPosition - nextPosition;
|
|
|
|
|
|
nextPosition = snappedPosition;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_previousPixelSnapDelta = Vector2.Zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the camera position.
|
|
|
|
|
|
GlobalPosition = nextPosition;
|
|
|
|
|
|
// IMPORTANT: Work around godot bug where camera doesn't update immediately: https://github.com/godotengine/godot/issues/74203
|
|
|
|
|
|
ForceUpdateScroll();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Vector2 GetAimDirection()
|
|
|
|
|
|
{
|
2025-05-07 12:01:02 +02:00
|
|
|
|
Vector2 screenSize = GetViewportRect().Size;
|
|
|
|
|
|
Vector2 aspectFix = new Vector2(1f, screenSize.X / screenSize.Y); // Stretch Y to match X range
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-05-07 11:36:03 +02:00
|
|
|
|
// Check controller stick input
|
|
|
|
|
|
Vector2 stickDir = new Vector2(
|
|
|
|
|
|
Input.GetActionStrength(AimRightName) - Input.GetActionStrength(AimLeftName),
|
|
|
|
|
|
Input.GetActionStrength(AimDownName) - Input.GetActionStrength(AimUpName)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
float stickLen = stickDir.Length();
|
|
|
|
|
|
if (stickLen > AimDeadzone)
|
|
|
|
|
|
{
|
|
|
|
|
|
float scaled = (stickLen - AimDeadzone) / (1f - AimDeadzone);
|
|
|
|
|
|
return stickDir.Normalized() * Mathf.Clamp(scaled, 0f, 1f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Mouse input
|
|
|
|
|
|
Vector2 screenCenter = GetViewportRect().Size / 2f;
|
|
|
|
|
|
Vector2 mousePos = GetViewport().GetMousePosition();
|
2025-05-07 12:01:02 +02:00
|
|
|
|
mousePos = mousePos.Clamp(Vector2.Zero, screenSize);
|
|
|
|
|
|
Vector2 dir = (mousePos - screenCenter) * aspectFix;
|
2025-05-07 11:36:03 +02:00
|
|
|
|
float dist = dir.Length();
|
|
|
|
|
|
|
|
|
|
|
|
// Use a pixel-based deadzone for mouse
|
|
|
|
|
|
float mouseDeadzone = AimDeadzone * 100f; // e.g. 0.2 * 100 = 20px
|
|
|
|
|
|
|
|
|
|
|
|
if (dist > mouseDeadzone)
|
|
|
|
|
|
{
|
|
|
|
|
|
float scaled = (dist - mouseDeadzone) / (screenCenter.Length() - mouseDeadzone);
|
|
|
|
|
|
return dir.Normalized() * Mathf.Clamp(scaled, 0f, 1f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Vector2.Zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public Vector2 GetPixelSnapDelta()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _previousPixelSnapDelta;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void RegisterTarget(CameraTarget target)
|
|
|
|
|
|
{
|
|
|
|
|
|
// assert(not _active_target)
|
|
|
|
|
|
_activeTarget = target;
|
|
|
|
|
|
_currentPosition = _activeTarget.GlobalPosition;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|