Files
Echo/Assets/_Project/Scripts/Player/PlayerController.cs
T
2026-07-08 20:42:51 +08:00

460 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<InputAction.CallbackContext> movePerformedHandler;
private System.Action<InputAction.CallbackContext> moveCanceledHandler;
private System.Action<InputAction.CallbackContext> lookPerformedHandler;
private System.Action<InputAction.CallbackContext> 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<CharacterController>();
playerInput = GetComponent<PlayerInput>(); // 获取组件
if (cameraTransform == null)
cameraTransform = GetComponentInChildren<Camera>()?.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<Vector2>();
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<Vector2>();
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("<color=green>[Player] Interact Action 绑定成功</color>");
}
else
{
Debug.LogError("<color=red>[Player] 找不到名为 'Interact' 的 Action!请检查 Input Actions 文件。</color>");
}
}
else
{
Debug.LogError("<color=red>[Player] PlayerInput 组件未找到!</color>");
}
}
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<IInteractable>();
if (interactablesOnHit != null && interactablesOnHit.Length > 0) hasInteractable = true;
else
{
var interactablesInParents = hitCollider.GetComponentsInParent<IInteractable>();
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<InteractConditionBehaviour>();
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($"<color=cyan>[Input] 按下了 Interact 键 (F)</color>");
PerformInteract();
}
}
private void PerformInteract()
{
if (cameraTransform == null)
{
Debug.LogError("<color=red>[Error] CameraTransform 为空!无法发射射线。</color>");
return;
}
Ray ray = new Ray(cameraTransform.position, cameraTransform.forward);
Debug.DrawRay(ray.origin, ray.direction * interactRange, Color.red, 2f); // 红色射线停留2秒
Debug.Log($"<color=yellow>[Raycast] 尝试发射射线... 起点:{ray.origin} 方向:{ray.direction}</color>");
if (Physics.Raycast(ray, out RaycastHit hit, interactRange, interactLayer))
{
Debug.Log($"<color=green>[Hit] 击中了物体: {hit.collider.name}</color> | Layer: {LayerMask.LayerToName(hit.collider.gameObject.layer)}");
var interactables = new List<IInteractable>(8);
var unique = new HashSet<IInteractable>();
var interactablesOnHit = hit.collider.GetComponents<IInteractable>();
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<IInteractable>();
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<InteractConditionBehaviour>();
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<Core.Triggers.InteractFailedTrigger>();
if (failedTriggers == null || failedTriggers.Length == 0)
{
Debug.LogWarning($"<color=orange>[Interact] 条件失败,但在 '{hit.collider.name}' 及其父级找不到 InteractFailedTrigger</color>");
}
for (int t = 0; t < failedTriggers.Length; t++)
{
if (failedTriggers[t] != null) failedTriggers[t].OnFailed(failReason);
}
if (!string.IsNullOrWhiteSpace(failReason))
{
Debug.LogWarning($"<color=orange>[Interact] 条件不满足:{failReason}</color>");
}
else
{
Debug.LogWarning("<color=orange>[Interact] 条件不满足</color>");
}
return;
}
}
Debug.Log($"<color=green>[Success] 找到 {interactables.Count} 个 IInteractable,准备依次调用 Interact()</color>");
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($"<color=orange>[Fail] 击中了 {hit.collider.name},但找不到 IInteractable 脚本 (也没在父物体找到)</color>");
}
}
else
{
Debug.Log("<color=grey>[Miss] 射线没有击中任何 Interact Layer 的物体</color>");
}
}
}
}