using System; using Cirno.Scripts.Utils; using Godot; namespace Cirno.Scripts.Misc; public partial class CameraController : Camera2D { public static CameraController Instance { get; private set; } [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; [Export] public float AimLerpSpeed { get; set; } = 0.8f; // How fast the offset adapts [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() { Instance = this; 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() { Vector2 screenSize = GetViewportRect().Size; Vector2 aspectFix = new Vector2(1f, screenSize.X / screenSize.Y); // Stretch Y to match X range // 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(); mousePos = mousePos.Clamp(Vector2.Zero, screenSize); Vector2 dir = (mousePos - screenCenter) * aspectFix; 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; } }