diff --git a/Scenes/Maps/Roguelike.tscn b/Scenes/Maps/Roguelike.tscn index 4ea36328..c35053e4 100644 --- a/Scenes/Maps/Roguelike.tscn +++ b/Scenes/Maps/Roguelike.tscn @@ -4,10 +4,10 @@ [ext_resource type="PackedScene" uid="uid://c4pr2707hbeph" path="res://Scenes/Actors/fsm_player.tscn" id="2_3fyis"] [ext_resource type="Resource" uid="uid://6ek4lmtuij4t" path="res://Resources/Maps/Roguelite.tres" id="2_k5t51"] [ext_resource type="Script" uid="uid://bt2qjgnf1wc2r" path="res://Scripts/Controllers/RogueliteRoomManager.cs" id="4_jtlua"] -[ext_resource type="Script" uid="uid://cfya7sndh7vy2" path="res://Scenes/CameraController.gd" id="8_y651a"] +[ext_resource type="Script" uid="uid://dd535g2nxqpg1" path="res://Scripts/Misc/CameraController.cs" id="6_gwtv6"] [ext_resource type="Script" uid="uid://c5nxsq3tyxcx6" path="res://Scripts/InventoryManager.cs" id="9_vhvs2"] +[ext_resource type="Script" uid="uid://upq0b4sx7nhw" path="res://Scripts/Misc/CameraTarget.cs" id="9_wbqvu"] [ext_resource type="PackedScene" uid="uid://dkwi1hu1bixoe" path="res://Scenes/HUD/HUD.tscn" id="10_6gk3e"] -[ext_resource type="Script" uid="uid://bdshph801ac2i" path="res://Scenes/CameraTarget.gd" id="11_4gy5m"] [ext_resource type="Script" uid="uid://cnkipcolyj61w" path="res://Scripts/AlarmManager.cs" id="12_eoca5"] [ext_resource type="PackedScene" uid="uid://b3tyacxxw88lx" path="res://Scenes/Utils/StreamPlayerWithName.tscn" id="13_4n7t6"] [ext_resource type="Script" uid="uid://3v6q0p5krqn7" path="res://Scripts/UI/Minimap.cs" id="16_pfafs"] @@ -30,9 +30,11 @@ MapThemes = Array[Object]([ExtResource("25_7gtqx")]) [node name="CameraController" type="Camera2D" parent="."] process_mode = 1 -script = ExtResource("8_y651a") -pixel_snap = false -enable_smoothing = false +script = ExtResource("6_gwtv6") +PixelSnap = false +EnableSmoothing = false +AimLerpSpeed = 0.8 +DebugCamera2ActionName = &"debug_camera_1" [node name="ReferenceRect" type="ReferenceRect" parent="."] visible = false @@ -48,7 +50,7 @@ script = ExtResource("9_vhvs2") [node name="CameraTarget" type="Node2D" parent="."] position = Vector2(154, 103) -script = ExtResource("11_4gy5m") +script = ExtResource("9_wbqvu") [node name="PlayerStartPosition" type="Marker2D" parent="."] position = Vector2(150, 80) diff --git a/Scenes/Weapons/BaseWeapon.tscn b/Scenes/Weapons/BaseWeapon.tscn index 6c8971ba..4555ff72 100644 --- a/Scenes/Weapons/BaseWeapon.tscn +++ b/Scenes/Weapons/BaseWeapon.tscn @@ -18,9 +18,8 @@ position = Vector2(-4.685, 3.52) [node name="ShootTimer" type="Timer" parent="."] one_shot = true -[node name="SoundModule" type="Node2D" parent="." node_paths=PackedStringArray("Weapon", "ShootSound", "ReloadSound", "EmptySound")] +[node name="SoundModule" type="Node2D" parent="." node_paths=PackedStringArray("ShootSound", "ReloadSound", "EmptySound")] script = ExtResource("2_uwnyl") -Weapon = NodePath("") ShootSound = NodePath("ShootSound") ReloadSound = NodePath("ReloadSound") EmptySound = NodePath("EmptySound") diff --git a/Scripts/Misc/CameraController.cs b/Scripts/Misc/CameraController.cs new file mode 100644 index 00000000..8ca2c7cd --- /dev/null +++ b/Scripts/Misc/CameraController.cs @@ -0,0 +1,172 @@ +using System; +using Cirno.Scripts.Utils; +using Godot; + +namespace Cirno.Scripts.Misc; + +public partial class CameraController : Camera2D +{ + [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; } = 10f; // 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() + { + 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() + { + // 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(); + Vector2 dir = mousePos - screenCenter; + 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; + } +} \ No newline at end of file diff --git a/Scripts/Misc/CameraController.cs.uid b/Scripts/Misc/CameraController.cs.uid new file mode 100644 index 00000000..ac6f5a3e --- /dev/null +++ b/Scripts/Misc/CameraController.cs.uid @@ -0,0 +1 @@ +uid://dd535g2nxqpg1 diff --git a/Scripts/Misc/CameraTarget.cs b/Scripts/Misc/CameraTarget.cs new file mode 100644 index 00000000..34f2982e --- /dev/null +++ b/Scripts/Misc/CameraTarget.cs @@ -0,0 +1,16 @@ +using Godot; + +namespace Cirno.Scripts.Misc; + +public partial class CameraTarget : Node2D +{ + public override void _Ready() + { + // register with the controller + var res = GetTree().GetFirstNodeInGroup("camera_controllers"); + if (res is not null && res is CameraController cameraController) + { + cameraController.RegisterTarget(this); + } + } +} \ No newline at end of file diff --git a/Scripts/Misc/CameraTarget.cs.uid b/Scripts/Misc/CameraTarget.cs.uid new file mode 100644 index 00000000..f07fd634 --- /dev/null +++ b/Scripts/Misc/CameraTarget.cs.uid @@ -0,0 +1 @@ +uid://upq0b4sx7nhw diff --git a/Scripts/Utils/MathFunctions.cs b/Scripts/Utils/MathFunctions.cs index 15bf7379..66ffb10d 100644 --- a/Scripts/Utils/MathFunctions.cs +++ b/Scripts/Utils/MathFunctions.cs @@ -1,4 +1,5 @@ -using Godot; +using System; +using Godot; namespace Cirno.Scripts.Utils; @@ -29,4 +30,35 @@ public static class MathFunctions return targetPos + targetVel * t; } + + // Critically damped spring, based on Game Programming Gems 4 Chapter 1.10. https://archive.org/details/game-programming-gems-4/page/95/mode/2up + // Returns a 2-tuple of [next_position, next_velocity]. + public static Tuple SmoothDamp(float current, float target, float currentVelocity, float smoothTime, float maxSpeed, float delta) + { + smoothTime = MathF.Max(smoothTime, 0.0001f); + var omega = 2.0f / smoothTime; + + var x = omega * delta; + var xExp = 1.0f / (1.0f + x + 0.48f * x * x + 0.235f * x * x * x); + var change = current - target; + var originalTarget = target; + + // Clamp max speed + var maxChange = maxSpeed * smoothTime; + change = Math.Clamp(change, -maxChange, maxChange); + target = current - change; + + var temp = (currentVelocity + omega * change) * delta; + currentVelocity = (currentVelocity - omega * temp) * xExp; + var output = target + (change + temp) * xExp; + + // Prevent Overshooting + if ((originalTarget - current > 0.0) == (output > originalTarget)) + { + output = originalTarget; + currentVelocity = (output - originalTarget) / delta; + } + + return new Tuple(output, currentVelocity); + } } \ No newline at end of file