using System.IO; using UnityEngine; using static UnityEngine.Debug; public class FlyCamera : MonoBehaviour { [Header("Movement Settings")] public float moveSpeed = 10f; public float fastMoveSpeed = 50f; public float mouseSensitivity = 2f; [Header("FOV Settings")] public float minFOV = 10f; public float maxFOV = 120f; public float fovScrollSpeed = 10f; [Header("Screenshot Settings")] public KeyCode screenshotKey = KeyCode.F2; [Header("Rough Mode")] [Tooltip("러프(느슨) 이동 모드. 켜면 이동/회전 입력을 즉각 반영하지 않고 둔하게 지연시켜 느슨하게 따라갑니다.")] public bool roughMode = false; [Tooltip("러프 모드 위치 추적 속도. 낮을수록 더 둔하고 묵직하게 따라갑니다.")] public float roughMoveSmoothing = 3f; [Tooltip("러프 모드 회전 추적 속도. 낮을수록 더 둔하고 묵직하게 따라갑니다.")] public float roughLookSmoothing = 5f; private Camera _cam; private float _rotationX; private float _rotationY; private Vector3 _targetPosition; private bool _isFlyCamEnabled = true; private float _screenshotFlashTime; private bool _showScreenshotFlash; private void Start() { _cam = GetComponent(); if (_cam == null) { _cam = Camera.main; } SyncRoughState(); Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; CreateScreenshotDirectory(); } private void Update() { if (!_isFlyCamEnabled) return; HandleMouseLook(); HandleMovement(); HandleFOVControl(); HandleScreenshot(); HandleScreenshotFlash(); if (Input.GetKeyDown(KeyCode.Escape)) { ToggleCursor(); } if (Cursor.lockState != CursorLockMode.Locked && Input.GetMouseButtonDown(0)) { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } } private void HandleMouseLook() { if (Cursor.lockState != CursorLockMode.Locked) return; float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity; float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity; _rotationX -= mouseY; _rotationX = Mathf.Clamp(_rotationX, -90f, 90f); _rotationY += mouseX; Quaternion targetRotation = Quaternion.Euler(_rotationX, _rotationY, 0f); // 러프 모드면 회전을 둔하게 지연시켜 따라가고, 아니면 즉각 반영 transform.rotation = roughMode ? Quaternion.Slerp(transform.rotation, targetRotation, roughLookSmoothing * Time.deltaTime) : targetRotation; } private void HandleMovement() { Vector3 movement = Vector3.zero; float currentSpeed = Input.GetKey(KeyCode.LeftShift) ? fastMoveSpeed : moveSpeed; if (Input.GetKey(KeyCode.W)) movement += transform.forward; if (Input.GetKey(KeyCode.S)) movement -= transform.forward; if (Input.GetKey(KeyCode.A)) movement -= transform.right; if (Input.GetKey(KeyCode.D)) movement += transform.right; if (Input.GetKey(KeyCode.Q)) movement -= transform.up; if (Input.GetKey(KeyCode.E)) movement += transform.up; Vector3 delta = movement * (currentSpeed * Time.deltaTime); if (roughMode) { // 입력은 목표 위치에 누적하고, 실제 위치는 둔하게 지연시켜 따라감 _targetPosition += delta; transform.position = Vector3.Lerp(transform.position, _targetPosition, roughMoveSmoothing * Time.deltaTime); } else { transform.position += delta; _targetPosition = transform.position; } } private void HandleFOVControl() { float scroll = Input.GetAxis("Mouse ScrollWheel"); if (scroll != 0f) { _cam.fieldOfView = Mathf.Clamp(_cam.fieldOfView - scroll * fovScrollSpeed, minFOV, maxFOV); } } private void HandleScreenshot() { if (Input.GetKeyDown(screenshotKey)) { TakeScreenshot(); } } private void CreateScreenshotDirectory() { string screenshotDir = Path.Combine(Application.dataPath, "..", "Screenshots"); if (!Directory.Exists(screenshotDir)) { Directory.CreateDirectory(screenshotDir); } } private void TakeScreenshot() { StartCoroutine(CaptureScreenshotDelayed()); } // ReSharper disable Unity.PerformanceAnalysis private System.Collections.IEnumerator CaptureScreenshotDelayed() { yield return new WaitForEndOfFrame(); string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); string filename = $"Screenshot_{timestamp}.png"; string screenshotDir = Path.Combine(Application.dataPath, "..", "Screenshots"); string fullPath = Path.Combine(screenshotDir, filename); ScreenCapture.CaptureScreenshot(fullPath); Log($"Screenshot saved: {fullPath}"); _showScreenshotFlash = true; _screenshotFlashTime = 0.2f; } private void HandleScreenshotFlash() { if (_showScreenshotFlash) { _screenshotFlashTime -= Time.deltaTime; if (_screenshotFlashTime <= 0f) { _showScreenshotFlash = false; } } } private void OnGUI() { if (_showScreenshotFlash) { GUI.color = new Color(1f, 1f, 1f, 0.8f); GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), Texture2D.whiteTexture); GUIStyle style = new GUIStyle(); style.fontSize = 36; style.normal.textColor = Color.black; style.alignment = TextAnchor.MiddleCenter; GUI.Label(new Rect(0, Screen.height / 2 - 50, Screen.width, 100), "Screenshot Captured!", style); } } private void ToggleCursor() { if (Cursor.lockState == CursorLockMode.Locked) { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } else { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } } public void EnableFlyCamera(bool enable) { _isFlyCamEnabled = enable; if (enable) { // 비활성화 동안 외부에서 카메라가 이동했을 수 있으므로 러프 모드 점프 방지를 위해 동기화 SyncRoughState(); Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } else { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } } /// /// 러프 모드의 목표 위치/회전 누적값을 현재 트랜스폼에 맞춰 동기화합니다. (점프 방지) /// private void SyncRoughState() { // eulerAngles.x는 0~360 범위라 위를 볼 때(예: 350°) 클램프에 걸려 튀므로 -180~180으로 정규화 float pitch = transform.eulerAngles.x; if (pitch > 180f) pitch -= 360f; _rotationX = Mathf.Clamp(pitch, -90f, 90f); _rotationY = transform.eulerAngles.y; _targetPosition = transform.position; } /// 러프(느슨) 이동 모드 활성 여부. public bool IsRoughMode => roughMode; /// 러프 이동 모드를 토글합니다. public void ToggleRoughMode() { SetRoughMode(!roughMode); } /// 러프 이동 모드를 명시적으로 켜거나 끕니다. public void SetRoughMode(bool enable) { if (roughMode == enable) return; // 토글 시점에 목표값을 현재 위치로 맞춰 전환 직후 튐 방지 SyncRoughState(); roughMode = enable; } }