Files
agame_for_unity/Assets/_Game/Scripts/PlayerController.cs
T
2026-09-20 17:09:58 +08:00

176 lines
7.0 KiB
C#

using FishNet.Component.Transforming;
using FishNet.Connection;
using FishNet.Object;
using UnityEngine;
using UnityEngine.InputSystem;
namespace Agame
{
/// <summary>
/// 玩家移动控制 —— FishNet 客户端授权 + Input System 的 PlayerInput / Actions 方案。
///
/// 输入链路:
/// Assets/_Game/Input/PlayerControls.inputactions
/// → Player 根节点上的 PlayerInput 组件(仅本地拥有者启用)
/// → 本脚本在 Update 里读取 Move / Sprint / Jump
///
/// 本脚本同时兼任「输入与相机的所有权开关」,见 ApplyOwnership 的注释。
///
/// FishNet 4.7.3 的三条硬约束(与输入方案无关,继续沿用):
/// 1. 必须继承 NetworkBehaviour,否则拿不到 IsOwner 与网络生命周期回调。
/// 2. IsOwner 禁止在 Awake / Start / OnStartServer / OnStartNetwork 中访问 ——
/// FishNet 的 ILPP 会直接报编译错误。
/// 3. 只有拥有者能驱动自己的对象,少了 IsOwner 判断就会去移动别人的玩家。
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class PlayerController : NetworkBehaviour
{
[Header("Movement")]
[SerializeField, Min(0f)] private float moveSpeed = 5f;
[SerializeField, Min(0f)] private float sprintSpeed = 8f;
[Header("Gravity & Jump")]
[Tooltip("负值向下。")]
[SerializeField] private float gravity = -20f;
[SerializeField, Min(0f)] private float jumpHeight = 1.5f;
[Header("Ownership")]
[Tooltip("仅本地拥有者需要启用的对象(玩家相机等)。非拥有者会自动禁用,避免多相机抢占渲染。")]
[SerializeField] private GameObject[] ownerOnlyObjects;
private CharacterController _controller;
private PlayerInput _playerInput;
private InputAction _moveAction;
private InputAction _jumpAction;
private InputAction _sprintAction;
private Vector3 _verticalVelocity;
private void Awake()
{
// Awake 中不得访问 IsOwner,这里只做纯本地缓存。
_controller = GetComponent<CharacterController>();
_playerInput = GetComponent<PlayerInput>();
if (_playerInput == null)
{
Debug.LogError(
$"[{nameof(PlayerController)}] {name} 上找不到 PlayerInput 组件,输入将不可用。" +
$"请在玩家预制体上加 PlayerInput,并把 Actions 指向 {PlayerInputNames.AssetFileName}。",
this);
return;
}
_moveAction = PlayerInputNames.Resolve(_playerInput, PlayerInputNames.Move, this);
_jumpAction = PlayerInputNames.Resolve(_playerInput, PlayerInputNames.Jump, this);
_sprintAction = PlayerInputNames.Resolve(_playerInput, PlayerInputNames.Sprint, this);
}
public override void OnStartClient()
{
base.OnStartClient();
// 用 OnStartClient 而不是 Awake/Start:走到这里说明对象已完成网络链接、
// 所有权已分配,此时 IsOwner 才是可信的。
ApplyOwnership();
WarnIfTransformNotSynchronized();
}
public override void OnOwnershipClient(NetworkConnection prevOwner)
{
base.OnOwnershipClient(prevOwner);
// 所有权可能中途变更(例如将来的主机迁移),输入与相机要跟着切换。
ApplyOwnership();
}
private void Update()
{
// 只驱动本地拥有者。
if (!IsOwner)
return;
Vector2 moveInput = _moveAction != null ? _moveAction.ReadValue<Vector2>() : Vector2.zero;
Vector3 moveDirection = BuildMoveDirection(moveInput);
bool isSprinting = _sprintAction != null && _sprintAction.IsPressed();
float speed = isSprinting ? sprintSpeed : moveSpeed;
_controller.Move(moveDirection * (speed * Time.deltaTime));
bool jumpPressed = _jumpAction != null && _jumpAction.WasPressedThisFrame();
UpdateJumpAndGravity(jumpPressed);
}
/// <summary>
/// 把 Move 的二维输入转换为相对于角色自身朝向的三维方向
/// (body 的偏航由 MouseLook 负责)。斜向输入做归一化,避免对角线移动比正向更快。
/// </summary>
private Vector3 BuildMoveDirection(Vector2 moveInput)
{
Vector3 direction = transform.right * moveInput.x + transform.forward * moveInput.y;
return Vector3.ClampMagnitude(direction, 1f);
}
private void UpdateJumpAndGravity(bool jumpPressed)
{
bool isGrounded = _controller.isGrounded;
// 贴地时保留一点向下的力,防止 isGrounded 在斜坡/台阶上反复抖动。
if (isGrounded && _verticalVelocity.y < 0f)
_verticalVelocity.y = -2f;
if (isGrounded && jumpPressed)
{
// 由目标跳跃高度反推初速度:v = sqrt(2 * g * h)
_verticalVelocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
_verticalVelocity.y += gravity * Time.deltaTime;
_controller.Move(_verticalVelocity * Time.deltaTime);
}
/// <summary>
/// 按所有权统一开关「只属于本地玩家」的东西:PlayerInput 与玩家相机。
///
/// 集中放在这里的原因:这两样都是「一个客户端只该有一份生效」。
/// 散到各脚本里各自 enable 容易漏,而且会出现顺序问题 ——
/// PlayerCamera 上的 MouseLook 在 OnEnable 里就要读 Look 动作,
/// 所以必须先开 PlayerInput、再启用相机,否则第一帧读不到值。
/// </summary>
private void ApplyOwnership()
{
if (_playerInput != null)
_playerInput.enabled = IsOwner;
if (ownerOnlyObjects == null)
return;
foreach (GameObject target in ownerOnlyObjects)
{
if (target != null)
target.SetActive(IsOwner);
}
}
/// <summary>
/// 缺少 NetworkTransform 时位置不会同步,但画面上"自己能走",极易误判成逻辑没问题。
/// 这里主动报一次,省掉对着两个人不同步干瞪眼的时间。
/// </summary>
private void WarnIfTransformNotSynchronized()
{
if (!IsOwner)
return;
if (GetComponent<NetworkTransform>() == null)
{
Debug.LogWarning(
$"[{nameof(PlayerController)}] {name} 上没有 NetworkTransform,位置不会同步到其他客户端。" +
"请在 Player 预制体上加 NetworkTransform,并开启 Client Authoritative 与 Synchronize Position。",
this);
}
}
}
}