using Godot; public partial class BoardCameraController : Camera3D { public override void _Ready() { InitBoardBounds(); InitOrbit(); } private void InitBoardBounds() { if (HexBoard != null && HexBoard.BoardBounds.Size != Vector2.Zero) { m_BoardRect = HexBoard.BoardBounds; var center2D = m_BoardRect.Position + m_BoardRect.Size * 0.5f; m_TargetPosition = new(center2D.X - 1, 0f, center2D.Y + 3); } else { m_BoardRect = new(-10, -10, 20, 20); m_TargetPosition = new(-1, 0f, 3); } var radius = Mathf.Max(m_BoardRect.Size.X, m_BoardRect.Size.Y) * 0.5f; m_MinDistance = MinZoomDistance; m_MaxDistance = Mathf.Max(m_MinDistance * 2f, radius * MaxZoomOutFactor); } private void InitOrbit() { var startYawRad = GlobalTransform.Basis.GetEuler().Y; var startYawDeg = Mathf.RadToDeg(startYawRad); m_RotationIndex = Mathf.PosMod(Mathf.RoundToInt(startYawDeg / 60f), 6); m_YawDegrees = m_RotationIndex * 60f; m_Distance = (m_MaxDistance - m_MinDistance) * 0.75f + m_MinDistance; UpdateTransformFromOrbit(); if (HexBoard != null) HexBoard.SetCameraRotationIndex(m_RotationIndex); } private void UpdateTransformFromOrbit() { var pitchRad = Mathf.DegToRad(PitchDegrees); var yawRad = Mathf.DegToRad(m_YawDegrees); var horizontal = m_Distance * Mathf.Cos(pitchRad); var height = m_Distance * Mathf.Sin(pitchRad); var offset = new Vector3(Mathf.Sin(yawRad) * horizontal, height, Mathf.Cos(yawRad) * horizontal); GlobalPosition = m_TargetPosition + offset; LookAt(m_TargetPosition, Vector3.Up); } public override void _Process(double delta) { var dt = (float)delta; HandleRotationInput(dt); HandleMovementInput(dt); HandleZoomInput(dt); } public override void _UnhandledInput(InputEvent @event) { if (@event is InputEventMouseButton mb && mb.Pressed) { if (mb.ButtonIndex == MouseButton.WheelUp) m_PendingWheelSteps -= 1f; else if (mb.ButtonIndex == MouseButton.WheelDown) m_PendingWheelSteps += 1f; } } private void HandleMovementInput(float delta) { var input = Vector2.Zero; input.Y -= Input.GetActionStrength("camera_move_forward"); input.Y += Input.GetActionStrength("camera_move_back"); input.X -= Input.GetActionStrength("camera_move_left"); input.X += Input.GetActionStrength("camera_move_right"); if (input.LengthSquared() < 0.0001f) return; var yawRad = Mathf.DegToRad(m_YawDegrees); var forward = new Vector2(Mathf.Sin(yawRad), Mathf.Cos(yawRad)); var right = new Vector2(forward.Y, -forward.X); var worldDir = right * input.X + forward * input.Y; var deltaPos = worldDir * MoveSpeed * delta; m_TargetPosition.X += deltaPos.X; m_TargetPosition.Z += deltaPos.Y; ClampTargetToBoard(); UpdateTransformFromOrbit(); } private void HandleZoomInput(float delta) { var zoomDelta = 0f; var axis = Input.GetActionStrength("camera_zoom_out") - Input.GetActionStrength("camera_zoom_in"); if (Mathf.Abs(axis) > 0.001f) zoomDelta += axis * PadZoomSpeed * delta; if (Mathf.Abs(m_PendingWheelSteps) > 0.001f) { zoomDelta += m_PendingWheelSteps * WheelZoomStep; m_PendingWheelSteps = 0f; } if (Mathf.Abs(zoomDelta) < 0.0001f) return; m_Distance = Mathf.Clamp(m_Distance + zoomDelta, m_MinDistance, m_MaxDistance); UpdateTransformFromOrbit(); } private void HandleRotationInput(float delta) { if (Input.IsActionJustPressed("camera_rotate_left")) QueueRotate(-1); if (Input.IsActionJustPressed("camera_rotate_right")) QueueRotate(+1); var axis = Input.GetActionStrength("camera_rotate_right") - Input.GetActionStrength("camera_rotate_left"); var axisDir = 0; if (axis > GamepadDeadZone) axisDir = +1; else if (axis < -GamepadDeadZone) axisDir = -1; if (axisDir != 0) { if (m_RotateAxisDir != axisDir) { m_RotateAxisDir = axisDir; m_RotateAxisTimer = 0f; QueueRotate(axisDir); } else { m_RotateAxisTimer += delta; if (m_RotateAxisTimer >= GamepadRotateRepeatDelay) { m_RotateAxisTimer = 0f; QueueRotate(axisDir); } } } else { m_RotateAxisDir = 0; m_RotateAxisTimer = 0f; } } private void QueueRotate(int steps) { if (steps == 0 || m_RotationTween != null) return; m_RotationIndex = Mathf.PosMod(m_RotationIndex + steps, 6); var targetYaw = m_RotationIndex * 60f; var current = m_YawDegrees; var deltaYaw = Mathf.Wrap(targetYaw - current, -180f, 180f); targetYaw = current + deltaYaw; HexBoard?.SetCameraRotationIndex(m_RotationIndex); m_RotationTween = CreateTween(); m_RotationTween.SetTrans(Tween.TransitionType.Back); m_RotationTween.SetEase(Tween.EaseType.Out); m_RotationTween.TweenProperty(this, nameof(YawDegrees), targetYaw, RotationDuration); m_RotationTween.TweenCallback(Callable.From(() => { YawDegrees = m_RotationIndex * 60f; m_RotationTween = null; })); } private void ClampTargetToBoard() { if (m_BoardRect.Size == Vector2.Zero) return; var minX = m_BoardRect.Position.X; var maxX = m_BoardRect.Position.X + m_BoardRect.Size.X; var minZ = m_BoardRect.Position.Y; var maxZ = m_BoardRect.Position.Y + m_BoardRect.Size.Y; m_TargetPosition.X = Mathf.Clamp(m_TargetPosition.X, minX, maxX); m_TargetPosition.Z = Mathf.Clamp(m_TargetPosition.Z, minZ, maxZ); } public float YawDegrees { get => m_YawDegrees; set { m_YawDegrees = value; UpdateTransformFromOrbit(); } } [Export] public HexBoard3D HexBoard { get; set; } [Export] public float GamepadDeadZone = 0.4f; [Export] public float GamepadRotateRepeatDelay = 1.0f; [Export] public float MaxZoomOutFactor = 2.2f; [ExportCategory("Zoom")] [Export] public float MinZoomDistance = 8f; [ExportCategory("Movement")] [Export] public float MoveSpeed = 10f; [Export] public float PadZoomSpeed = 40f; [ExportCategory("Rotation")] [Export] public float PitchDegrees = 60f; [Export] public float RotationDuration = 0.35f; [Export] public float WheelZoomStep = 3f; private Rect2 m_BoardRect; private float m_Distance; private float m_MaxDistance; private float m_MinDistance; private float m_PendingWheelSteps; private int m_RotateAxisDir; private float m_RotateAxisTimer; private int m_RotationIndex; private Tween m_RotationTween; private Vector3 m_TargetPosition = new(-1, 0f, 3); private float m_YawDegrees; }