Files

460 lines
18 KiB
C#
Raw Permalink Normal View History

2026-07-08 19:53:15 +08:00
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>");
}
}
}
}