using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; using Interaction.Conditions; using Michsky.UI.Reach; namespace Player { [RequireComponent(typeof(CharacterController))] [RequireComponent(typeof(PlayerInput))] public class PlayerController : MonoBehaviour { [Header("Movement Settings")] [SerializeField] private float moveSpeed = 5f; [SerializeField] private float jumpHeight = 1.2f; [SerializeField] private float gravity = -9.81f; [Header("Look Settings")] [SerializeField] private float mouseSensitivity = 1.2f; [SerializeField] private Transform camPivot; // 摄像机支架(用于上下看) [SerializeField] private float maxLookAngle = 80f; // 抬头低头限制 [Header("Interaction Settings")] [SerializeField] private float interactRange = 3f; [SerializeField] private LayerMask interactLayer; [SerializeField] private Transform cameraTransform; [Header("Interaction Prompt UI")] [SerializeField] private bool enableAimPrompt = true; [SerializeField] private FeedNotification canInteractNotification; [SerializeField] private FeedNotification blockedNotification; [SerializeField] private float aimPromptRefreshInterval = 0.05f; [SerializeField] private bool blockedUseFailReason = true; [Header("References")] [SerializeField] private Transform bodyTransform; private CharacterController characterController; private PlayerInput playerInput; // 引用 PlayerInput private Vector2 moveInput; private Vector2 lookInput; private float xRotation = 0f; private Vector3 playerVelocity; private bool isGrounded; private System.Action movePerformedHandler; private System.Action moveCanceledHandler; private System.Action lookPerformedHandler; private System.Action lookCanceledHandler; private enum AimPromptState { None = 0, Allowed = 1, Blocked = 2 } private AimPromptState aimPromptState; private Collider lastAimCollider; private string lastBlockedReason; private float nextAimPromptTime; private string canInteractOriginalText; private string blockedOriginalText; public void ResetInputState() { moveInput = Vector2.zero; lookInput = Vector2.zero; } public void SetMouseSensitivity(float value) { float v = Mathf.Max(0f, value); if (Mathf.Approximately(mouseSensitivity, v)) { return; } mouseSensitivity = v; Debug.Log($"[Player] MouseSensitivity set to {mouseSensitivity}"); } private void Awake() { characterController = GetComponent(); playerInput = GetComponent(); // 获取组件 if (cameraTransform == null) cameraTransform = GetComponentInChildren()?.transform; if (camPivot == null && cameraTransform != null) camPivot = cameraTransform.parent; if (bodyTransform == null) bodyTransform = transform.Find("Body"); // 锁定鼠标光标 Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; if (canInteractNotification != null) canInteractOriginalText = canInteractNotification.notificationText; if (blockedNotification != null) blockedOriginalText = blockedNotification.notificationText; } private void OnEnable() { // --- 显式订阅 Input System 事件 --- // 这种方式比 SendMessage 更明确,不容易出错 if (playerInput != null) { // 确保你的 Action Map 名字正确 (默认通常是 "Player") var moveAction = playerInput.actions["Move"]; if (moveAction != null) { if (movePerformedHandler == null) movePerformedHandler = ctx => moveInput = ctx.ReadValue(); if (moveCanceledHandler == null) moveCanceledHandler = _ => moveInput = Vector2.zero; moveAction.performed += movePerformedHandler; moveAction.canceled += moveCanceledHandler; } var lookAction = playerInput.actions["Look"]; if (lookAction != null) { if (lookPerformedHandler == null) lookPerformedHandler = ctx => lookInput = ctx.ReadValue(); if (lookCanceledHandler == null) lookCanceledHandler = _ => lookInput = Vector2.zero; lookAction.performed += lookPerformedHandler; lookAction.canceled += lookCanceledHandler; } var jumpAction = playerInput.actions["Jump"]; if (jumpAction != null) { jumpAction.performed += OnJump; } var interactAction = playerInput.actions["Interact"]; if (interactAction != null) { interactAction.performed += OnInteract; Debug.Log("[Player] Interact Action 绑定成功"); } else { Debug.LogError("[Player] 找不到名为 'Interact' 的 Action!请检查 Input Actions 文件。"); } } else { Debug.LogError("[Player] PlayerInput 组件未找到!"); } } private void OnDisable() { // 记得取消订阅,防止内存泄漏或报错 if (playerInput != null) { var moveAction = playerInput.actions["Move"]; if (moveAction != null) { if (movePerformedHandler != null) moveAction.performed -= movePerformedHandler; if (moveCanceledHandler != null) moveAction.canceled -= moveCanceledHandler; } var lookAction = playerInput.actions["Look"]; if (lookAction != null) { if (lookPerformedHandler != null) lookAction.performed -= lookPerformedHandler; if (lookCanceledHandler != null) lookAction.canceled -= lookCanceledHandler; } var jumpAction = playerInput.actions["Jump"]; if (jumpAction != null) jumpAction.performed -= OnJump; var interactAction = playerInput.actions["Interact"]; if (interactAction != null) interactAction.performed -= OnInteract; } ResetInputState(); } private void Update() { HandleMovement(); HandleLook(); UpdateAimPrompt(); } private void UpdateAimPrompt() { if (!enableAimPrompt) return; if (cameraTransform == null) return; if (aimPromptRefreshInterval > 0f && Time.time < nextAimPromptTime) return; nextAimPromptTime = aimPromptRefreshInterval > 0f ? Time.time + aimPromptRefreshInterval : Time.time; Ray ray = new Ray(cameraTransform.position, cameraTransform.forward); if (!Physics.Raycast(ray, out RaycastHit hit, interactRange, interactLayer)) { SetAimPromptState(AimPromptState.None, null, null); return; } var hitCollider = hit.collider; bool hasInteractable = false; var interactablesOnHit = hitCollider.GetComponents(); if (interactablesOnHit != null && interactablesOnHit.Length > 0) hasInteractable = true; else { var interactablesInParents = hitCollider.GetComponentsInParent(); if (interactablesInParents != null && interactablesInParents.Length > 0) hasInteractable = true; } if (!hasInteractable) { SetAimPromptState(AimPromptState.None, hitCollider, null); return; } bool allowed = true; string failReason = null; var conditions = hitCollider.GetComponentsInParent(); for (int i = 0; i < conditions.Length; i++) { var condition = conditions[i]; if (condition == null) continue; if (!condition.CanInteract(gameObject, out failReason)) { allowed = false; break; } } SetAimPromptState(allowed ? AimPromptState.Allowed : AimPromptState.Blocked, hitCollider, failReason); } private void SetAimPromptState(AimPromptState state, Collider collider, string failReason) { bool colliderChanged = lastAimCollider != collider; bool stateChanged = aimPromptState != state; bool reasonChanged = (state == AimPromptState.Blocked) && lastBlockedReason != failReason; lastAimCollider = collider; lastBlockedReason = failReason; if (!colliderChanged && !stateChanged && !reasonChanged) return; aimPromptState = state; if (state == AimPromptState.Allowed) { SetNotificationActiveFast(blockedNotification, false); if (canInteractNotification != null) { canInteractNotification.UpdateUI(); SetNotificationActiveFast(canInteractNotification, true); } return; } if (state == AimPromptState.Blocked) { SetNotificationActiveFast(canInteractNotification, false); if (blockedNotification != null) { if (blockedUseFailReason) { string text = string.IsNullOrWhiteSpace(failReason) ? blockedOriginalText : failReason; if (!string.IsNullOrEmpty(text)) { blockedNotification.notificationText = text; blockedNotification.UpdateUI(); } } SetNotificationActiveFast(blockedNotification, true); } return; } if (canInteractNotification != null) { if (!string.IsNullOrEmpty(canInteractOriginalText)) { canInteractNotification.notificationText = canInteractOriginalText; canInteractNotification.UpdateUI(); } SetNotificationActiveFast(canInteractNotification, false); } if (blockedNotification != null) { if (!string.IsNullOrEmpty(blockedOriginalText)) { blockedNotification.notificationText = blockedOriginalText; blockedNotification.UpdateUI(); } SetNotificationActiveFast(blockedNotification, false); } } private static void SetNotificationActiveFast(FeedNotification notification, bool active) { if (notification == null) return; // 绕过 ReachUI 内部的协程动画,直接控制 GameObject 显隐 // 注意:因为我们不再使用 ExpandNotification/MinimizeNotification, // 需确保 Animator 已经被移除或禁用,以免残留的动画覆盖我们的显示状态。 notification.gameObject.SetActive(active); } private void HandleLook() { float mouseX = lookInput.x * mouseSensitivity * Time.deltaTime; float mouseY = lookInput.y * mouseSensitivity * Time.deltaTime; // 1. 左右旋转 (旋转整个角色身体) transform.Rotate(Vector3.up * mouseX); // 2. 上下旋转 (只旋转摄像机支架) if (camPivot != null) { xRotation -= mouseY; xRotation = Mathf.Clamp(xRotation, -maxLookAngle, maxLookAngle); camPivot.localRotation = Quaternion.Euler(xRotation, 0f, 0f); } } private void HandleMovement() { isGrounded = characterController.isGrounded; if (isGrounded && playerVelocity.y < 0) { playerVelocity.y = -2f; } // --- 处理移动 --- // 直接使用 transform.right/forward,因为身体已经跟随鼠标旋转了 Vector3 move = transform.right * moveInput.x + transform.forward * moveInput.y; characterController.Move(move * moveSpeed * Time.deltaTime); // --- 处理重力 --- playerVelocity.y += gravity * Time.deltaTime; characterController.Move(playerVelocity * Time.deltaTime); } // --- Input System Events --- // 注意:现在不需要 public void OnMove(...) 这种格式了,因为我们已经在 OnEnable 里手动订阅了 public void OnJump(InputAction.CallbackContext context) { if (context.performed && isGrounded) { playerVelocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); } } public void OnInteract(InputAction.CallbackContext context) { if (context.performed) { Debug.Log($"[Input] 按下了 Interact 键 (F)"); PerformInteract(); } } private void PerformInteract() { if (cameraTransform == null) { Debug.LogError("[Error] CameraTransform 为空!无法发射射线。"); return; } Ray ray = new Ray(cameraTransform.position, cameraTransform.forward); Debug.DrawRay(ray.origin, ray.direction * interactRange, Color.red, 2f); // 红色射线停留2秒 Debug.Log($"[Raycast] 尝试发射射线... 起点:{ray.origin} 方向:{ray.direction}"); if (Physics.Raycast(ray, out RaycastHit hit, interactRange, interactLayer)) { Debug.Log($"[Hit] 击中了物体: {hit.collider.name} | Layer: {LayerMask.LayerToName(hit.collider.gameObject.layer)}"); var interactables = new List(8); var unique = new HashSet(); var interactablesOnHit = hit.collider.GetComponents(); for (int i = 0; i < interactablesOnHit.Length; i++) { var it = interactablesOnHit[i]; if (it == null) continue; if (unique.Add(it)) interactables.Add(it); } var interactablesInParents = hit.collider.GetComponentsInParent(); for (int i = 0; i < interactablesInParents.Length; i++) { var it = interactablesInParents[i]; if (it == null) continue; if (unique.Add(it)) interactables.Add(it); } if (interactables.Count > 0) { var conditions = hit.collider.GetComponentsInParent(); for (int i = 0; i < conditions.Length; i++) { var condition = conditions[i]; if (condition == null) continue; if (!condition.CanInteract(gameObject, out string failReason)) { var failedTriggers = hit.collider.GetComponentsInParent(); if (failedTriggers == null || failedTriggers.Length == 0) { Debug.LogWarning($"[Interact] 条件失败,但在 '{hit.collider.name}' 及其父级找不到 InteractFailedTrigger"); } for (int t = 0; t < failedTriggers.Length; t++) { if (failedTriggers[t] != null) failedTriggers[t].OnFailed(failReason); } if (!string.IsNullOrWhiteSpace(failReason)) { Debug.LogWarning($"[Interact] 条件不满足:{failReason}"); } else { Debug.LogWarning("[Interact] 条件不满足"); } return; } } Debug.Log($"[Success] 找到 {interactables.Count} 个 IInteractable,准备依次调用 Interact()"); for (int i = 0; i < interactables.Count; i++) { var interactable = interactables[i]; if (interactable == null) continue; interactable.Interact(); } for (int i = 0; i < conditions.Length; i++) { var condition = conditions[i]; if (condition == null) continue; condition.OnInteractSucceeded(gameObject); } } else { Debug.LogWarning($"[Fail] 击中了 {hit.collider.name},但找不到 IInteractable 脚本 (也没在父物体找到)"); } } else { Debug.Log("[Miss] 射线没有击中任何 Interact Layer 的物体"); } } } }