From d2ec2f6f0e93e9253a4608eb822540b7da7f1a89 Mon Sep 17 00:00:00 2001 From: yvmou Date: Sun, 20 Sep 2026 15:54:34 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/DefaultPrefabObjects.asset | 2 +- .../cecil-0.11.4/Mono.Cecil.sln.meta | 14 -- .../Tugboat/LiteNetLib/LiteNetLib.csproj.meta | 14 -- Assets/_Game/Prefabs/Player.prefab | 60 ++++++- Assets/_Game/Scenes/MainScene.unity | 145 ++++++++--------- Assets/_Game/Scripts/MouseLook.cs | 69 +++++--- Assets/_Game/Scripts/PlayerController.cs | 150 ++++++++++++++---- 7 files changed, 288 insertions(+), 166 deletions(-) delete mode 100644 Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln.meta delete mode 100644 Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/LiteNetLib.csproj.meta diff --git a/Assets/DefaultPrefabObjects.asset b/Assets/DefaultPrefabObjects.asset index 0f1ae88..3b5f8aa 100644 --- a/Assets/DefaultPrefabObjects.asset +++ b/Assets/DefaultPrefabObjects.asset @@ -21,8 +21,8 @@ MonoBehaviour: - {fileID: 201277550, guid: 5b712878ecece354ba4ffb026c0a221c, type: 3} - {fileID: 4512293259955182956, guid: b8017cef39731ba439c70fecc09488e3, type: 3} - {fileID: 201277550, guid: 26a567abbe21227428f5c3d309d1d73c, type: 3} + - {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - {fileID: 4512293259955182956, guid: 44611030e61220d42ab7c37ba3c0ea92, type: 3} - {fileID: 8192566354860284824, guid: 6331b3542e64a564c81bc39cedf70c8d, type: 3} - {fileID: 4512293259955182956, guid: f32d4c19de900e64cb73cedcb8ba6f70, type: 3} - {fileID: 611616139817875448, guid: bf5f023b4017a5e41a9815ec5745df3d, type: 3} - - {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} diff --git a/Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln.meta b/Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln.meta deleted file mode 100644 index a534b60..0000000 --- a/Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln.meta +++ /dev/null @@ -1,14 +0,0 @@ -fileFormatVersion: 2 -guid: 246f31a0e00fea74a93125fec6d80da8 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 207815 - packageName: 'FishNet: Networking Evolved' - packageVersion: 4.7.3R - assetPath: Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln - uploadId: 987062 diff --git a/Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/LiteNetLib.csproj.meta b/Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/LiteNetLib.csproj.meta deleted file mode 100644 index 667520a..0000000 --- a/Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/LiteNetLib.csproj.meta +++ /dev/null @@ -1,14 +0,0 @@ -fileFormatVersion: 2 -guid: 1ec83424c5410b04aab06dddca60e7f6 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 207815 - packageName: 'FishNet: Networking Evolved' - packageVersion: 4.7.3R - assetPath: Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/LiteNetLib.csproj - uploadId: 987062 diff --git a/Assets/_Game/Prefabs/Player.prefab b/Assets/_Game/Prefabs/Player.prefab index a203afe..a265caa 100644 --- a/Assets/_Game/Prefabs/Player.prefab +++ b/Assets/_Game/Prefabs/Player.prefab @@ -18,7 +18,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!4 &1098286384477188469 Transform: m_ObjectHideFlags: 0 @@ -160,6 +160,7 @@ GameObject: - component: {fileID: 3905363232486332009} - component: {fileID: -8731164649411638372} - component: {fileID: -1605824257395221361} + - component: {fileID: -1778631990423174556} m_Layer: 0 m_Name: Player m_TagString: Player @@ -277,10 +278,15 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: b6780c1c0b9d27c46a0bd43f0d2291f6, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::Agame.PlayerController + _componentIndexCache: 0 + _addedNetworkObject: {fileID: -8731164649411638372} + _networkObjectCache: {fileID: -8731164649411638372} moveSpeed: 5 sprintSpeed: 8 gravity: -20 jumpHeight: 1.5 + ownerOnlyObjects: + - {fileID: 6595615933852889331} --- !u!114 &-8731164649411638372 MonoBehaviour: m_ObjectHideFlags: 0 @@ -327,7 +333,9 @@ MonoBehaviour: k__BackingField: {fileID: 0} k__BackingField: {fileID: 0} NetworkBehaviours: + - {fileID: 3905363232486332009} - {fileID: -1605824257395221361} + - {fileID: -1778631990423174556} InitializedParentNetworkBehaviour: {fileID: 0} InitializedNestedNetworkObjects: [] RuntimeParentNetworkBehaviour: {fileID: 0} @@ -338,7 +346,7 @@ MonoBehaviour: _initializeOrder: 0 _preventDespawnOnDisconnect: 0 _defaultDespawnType: 0 - _initializedTimestamp: -8584119494375252787 + _initializedTimestamp: -8584117169363086877 --- !u!114 &-1605824257395221361 MonoBehaviour: m_ObjectHideFlags: 0 @@ -351,6 +359,52 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8a6a39c46bf52104ba8efe3100bce3f7, type: 3} m_Name: m_EditorClassIdentifier: FishNet.Runtime::FishNet.Object.EmptyNetworkBehaviour - _componentIndexCache: 0 + _componentIndexCache: 1 _addedNetworkObject: {fileID: -8731164649411638372} _networkObjectCache: {fileID: -8731164649411638372} +--- !u!114 &-1778631990423174556 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7344258574652498121} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a2836e36774ca1c4bbbee976e17b649c, type: 3} + m_Name: + m_EditorClassIdentifier: FishNet.Runtime::FishNet.Component.Transforming.NetworkTransform + _componentIndexCache: 2 + _addedNetworkObject: {fileID: -8731164649411638372} + _networkObjectCache: {fileID: -8731164649411638372} + _componentConfiguration: 0 + _synchronizeParent: 0 + _packing: + Position: 1 + Rotation: 1 + Scale: 0 + _useScaledTime: 1 + _interpolation: 2 + _extrapolation: 2 + _enableTeleport: 0 + _teleportThreshold: 1 + _clientAuthoritative: 1 + _sendToOwner: 1 + _interval: 1 + _synchronizePosition: 1 + _positionSensitivity: 0.001 + _positionSnapping: + X: 0 + Y: 0 + Z: 0 + _synchronizeRotation: 1 + _rotationSnapping: + X: 0 + Y: 0 + Z: 0 + _synchronizeScale: 1 + _scaleSensitivity: 0.001 + _scaleSnapping: + X: 0 + Y: 0 + Z: 0 diff --git a/Assets/_Game/Scenes/MainScene.unity b/Assets/_Game/Scenes/MainScene.unity index d298968..b3ee6d3 100644 --- a/Assets/_Game/Scenes/MainScene.unity +++ b/Assets/_Game/Scenes/MainScene.unity @@ -150,6 +150,71 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1001 &383713395 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: SceneId + value: 2952396223 + objectReference: {fileID: 0} + - target: {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: WasActiveDuringEdit + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalPosition.x + value: 7.09624 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalPosition.y + value: 2.94 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalPosition.z + value: -10.98281 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7344258574652498121, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} + propertyPath: m_Name + value: Player + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} --- !u!1 &870123117 GameObject: m_ObjectHideFlags: 0 @@ -367,7 +432,7 @@ PrefabInstance: - target: {fileID: 7443408887813606060, guid: 0b650fca685f2eb41a86538aa883e4c1, type: 3} propertyPath: _playerPrefab value: - objectReference: {fileID: 1929457866} + objectReference: {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - target: {fileID: 7443408887813606060, guid: 0b650fca685f2eb41a86538aa883e4c1, type: 3} propertyPath: Spawns.Array.size value: 2 @@ -497,82 +562,6 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &1929457866 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - m_PrefabInstance: {fileID: 5836281772481032079} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 26b716c41e9b56b4baafaf13a523ba2e, type: 3} - m_Name: - m_EditorClassIdentifier: FishNet.Runtime::FishNet.Object.NetworkObject ---- !u!1001 &5836281772481032079 -PrefabInstance: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_Modification: - serializedVersion: 3 - m_TransformParent: {fileID: 0} - m_Modifications: - - target: {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: SceneId - value: 4160487019 - objectReference: {fileID: 0} - - target: {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: WasActiveDuringEdit - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalPosition.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalPosition.y - value: 2 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalPosition.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalRotation.w - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalRotation.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalRotation.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalRotation.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalEulerAnglesHint.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalEulerAnglesHint.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4433491345327751435, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_LocalEulerAnglesHint.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7344258574652498121, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} - propertyPath: m_Name - value: Player - objectReference: {fileID: 0} - m_RemovedComponents: [] - m_RemovedGameObjects: [] - m_AddedGameObjects: [] - m_AddedComponents: [] - m_SourcePrefab: {fileID: 100100000, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3} --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 @@ -580,6 +569,6 @@ SceneRoots: - {fileID: 870123120} - {fileID: 1617942571} - {fileID: 1749431010} - - {fileID: 5836281772481032079} - {fileID: 277106704} - {fileID: 1541499318} + - {fileID: 383713395} diff --git a/Assets/_Game/Scripts/MouseLook.cs b/Assets/_Game/Scripts/MouseLook.cs index 022e179..cb6b729 100644 --- a/Assets/_Game/Scripts/MouseLook.cs +++ b/Assets/_Game/Scripts/MouseLook.cs @@ -3,65 +3,82 @@ using UnityEngine.InputSystem; namespace Agame { + /// + /// 第一人称视角控制:鼠标横移带动 body 偏航,纵移带动相机俯仰。 + /// + /// 网络侧前提(重要):本组件挂在 PlayerCamera 上,而 PlayerCamera 只会被 + /// 为「本地拥有者」启用(见其 ApplyOwnership)。 + /// 非拥有者客户端的相机是 disabled 的,因此这里不会执行,天然不需要再判所有权。 + /// + /// 反过来说:如果日后把 PlayerCamera 改成常驻启用,必须补上所有权判断, + /// 否则每个客户端都会用自己的鼠标去转动别人玩家的视角。 + /// public class MouseLook : MonoBehaviour { [Header("Sensitivity")] [Tooltip("Degrees of rotation per pixel of mouse movement.")] - public float mouseSensitivity = 0.12f; + [SerializeField] private float mouseSensitivity = 0.12f; [Header("Vertical Clamp")] - public float minLookAngle = -80f; - public float maxLookAngle = 80f; + [SerializeField] private float minLookAngle = -80f; + [SerializeField] private float maxLookAngle = 80f; [Header("References")] - public Transform characterBody; + [Tooltip("被偏航(Horizontal)的躯体 Transform,通常是玩家根节点。")] + [SerializeField] private Transform characterBody; - private float xRotation = 0f; + private float _xRotation; + private bool _warnedMissingBody; - private void Start() + private void OnEnable() { + // 跟随本对象的启用状态锁定光标:只有本地拥有者的相机会被启用, + // 所以光标锁定/释放自然就只在拥有者这一侧发生。 LockCursor(true); } + private void OnDisable() + { + // 对象被禁用(非拥有者,或玩家已下线)时务必释放光标,否则会锁死在别人的机器上。 + LockCursor(false); + } + private void Update() { - var kb = Keyboard.current; - var mouse = Mouse.current; + Mouse mouse = Mouse.current; if (mouse == null) - { return; - } - // Esc releases the cursor, left click grabs it again (editor-friendly) + // Esc 释放光标,左键重新抓取(方便编辑器里切出去)。 + Keyboard kb = Keyboard.current; if (kb != null && kb.escapeKey.wasPressedThisFrame) - { LockCursor(false); - } + if (mouse.leftButton.wasPressedThisFrame && Cursor.lockState != CursorLockMode.Locked) - { LockCursor(true); - } if (Cursor.lockState != CursorLockMode.Locked) - { return; - } - // Mouse delta is in pixels for the new Input System + // 新输入系统下 delta 的单位是像素,不是轴值。 Vector2 delta = mouse.delta.ReadValue(); - float mouseX = delta.x * mouseSensitivity; - float mouseY = delta.y * mouseSensitivity; - // Yaw the body left/right + // 横移:转动躯体 if (characterBody != null) { - characterBody.Rotate(Vector3.up * mouseX); + characterBody.Rotate(Vector3.up * (delta.x * mouseSensitivity)); + } + else if (!_warnedMissingBody) + { + _warnedMissingBody = true; + Debug.LogWarning( + $"[{nameof(MouseLook)}] 未指定 Character Body,水平视角不会生效。请指向玩家根节点的 Transform。", + this); } - // Pitch the camera up/down - xRotation -= mouseY; - xRotation = Mathf.Clamp(xRotation, minLookAngle, maxLookAngle); - transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f); + // 纵移:俯仰相机,并夹在上下限之间防止翻转 + _xRotation = Mathf.Clamp(_xRotation - (delta.y * mouseSensitivity), minLookAngle, maxLookAngle); + transform.localRotation = Quaternion.Euler(_xRotation, 0f, 0f); } private void LockCursor(bool locked) diff --git a/Assets/_Game/Scripts/PlayerController.cs b/Assets/_Game/Scripts/PlayerController.cs index 3c36439..eee8966 100644 --- a/Assets/_Game/Scripts/PlayerController.cs +++ b/Assets/_Game/Scripts/PlayerController.cs @@ -1,64 +1,154 @@ +using FishNet.Component.Transforming; +using FishNet.Connection; +using FishNet.Object; using UnityEngine; using UnityEngine.InputSystem; namespace Agame { + /// + /// 玩家移动控制 —— FishNet 客户端授权模式。 + /// 参考:Docs/联机方案选型.md §3.3,以及官方教程 "Moving Your Player Around"。 + /// + /// 职责边界:本组件只负责「读输入 + 驱动 CharacterController」。 + /// 位置/旋转的同步交给 Player 预制体上的 NetworkTransform(Client Authoritative), + /// 这里不写任何 RPC / SyncVar。 + /// + /// FishNet 4.7.3 下的三条硬约束: + /// 1. 必须继承 NetworkBehaviour 而非 MonoBehaviour,否则拿不到 IsOwner 与网络生命周期回调。 + /// 2. IsOwner 禁止在 Awake / Start / OnStartServer / OnStartNetwork 里访问 —— + /// FishNet 代码生成器会直接抛编译错误,按拥有者初始化的逻辑要放在 OnStartClient。 + /// 3. 只有拥有者能驱动自己的对象,少了这道判断就会去移动别人的玩家。 + /// [RequireComponent(typeof(CharacterController))] - public class PlayerController : MonoBehaviour + public class PlayerController : NetworkBehaviour { [Header("Movement")] - public float moveSpeed = 5f; - public float sprintSpeed = 8f; - public float gravity = -20f; - public float jumpHeight = 1.5f; + [SerializeField, Min(0f)] private float moveSpeed = 5f; + [SerializeField, Min(0f)] private float sprintSpeed = 8f; - private CharacterController controller; - private Vector3 velocity; - private bool isGrounded; + [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 Vector3 _verticalVelocity; private void Awake() { - controller = GetComponent(); + // Awake 中不得访问 IsOwner,这里只做纯本地缓存。 + _controller = GetComponent(); + } + + 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() { - var kb = Keyboard.current; - if (kb == null) - { + // 只驱动本地拥有者,否则每个客户端都会去移动别人的玩家。 + if (!IsOwner) return; - } - isGrounded = controller.isGrounded; - if (isGrounded && velocity.y < 0f) - { - velocity.y = -2f; - } + Keyboard kb = Keyboard.current; + if (kb == null) + return; - // WASD input + float speed = kb.leftShiftKey.isPressed ? sprintSpeed : moveSpeed; + Vector3 moveDirection = ReadMoveDirection(kb); + + _controller.Move(moveDirection * (speed * Time.deltaTime)); + + UpdateJumpAndGravity(kb); + } + + /// + /// 读取 WASD 并转换为相对于角色自身朝向的方向(body 的偏航由 MouseLook 负责)。 + /// 斜向输入做归一化,避免对角线移动比正向更快。 + /// + private Vector3 ReadMoveDirection(Keyboard kb) + { float moveX = 0f; float moveZ = 0f; + if (kb.aKey.isPressed) moveX -= 1f; if (kb.dKey.isPressed) moveX += 1f; if (kb.sKey.isPressed) moveZ -= 1f; if (kb.wKey.isPressed) moveZ += 1f; - // Move relative to the capsule's own facing (which MouseLook rotates) - Vector3 move = transform.right * moveX + transform.forward * moveZ; - move = Vector3.ClampMagnitude(move, 1f); + Vector3 direction = transform.right * moveX + transform.forward * moveZ; + return Vector3.ClampMagnitude(direction, 1f); + } - float speed = kb.leftShiftKey.isPressed ? sprintSpeed : moveSpeed; - controller.Move(move * speed * Time.deltaTime); + private void UpdateJumpAndGravity(Keyboard kb) + { + bool isGrounded = _controller.isGrounded; - // Jump - if (kb.spaceKey.wasPressedThisFrame && isGrounded) + // 贴地时保留一点向下的力,防止 isGrounded 在斜坡/台阶上反复抖动。 + if (isGrounded && _verticalVelocity.y < 0f) + _verticalVelocity.y = -2f; + + if (isGrounded && kb.spaceKey.wasPressedThisFrame) { - velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); + // 由目标跳跃高度反推初速度:v = sqrt(2 * g * h) + _verticalVelocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); } - // Gravity - velocity.y += gravity * Time.deltaTime; - controller.Move(velocity * Time.deltaTime); + _verticalVelocity.y += gravity * Time.deltaTime; + _controller.Move(_verticalVelocity * Time.deltaTime); + } + + /// + /// 按所有权开关"只属于本地玩家"的对象。最典型的是玩家相机: + /// 若每个客户端的相机都常驻启用,N 个玩家就会互相抢渲染。 + /// + private void ApplyOwnership() + { + if (ownerOnlyObjects == null) + return; + + foreach (GameObject target in ownerOnlyObjects) + { + if (target != null) + target.SetActive(IsOwner); + } + } + + /// + /// 缺少 NetworkTransform 时位置不会同步,但画面上"自己能走",极易误判成逻辑没问题。 + /// 这里主动报一次,省掉对着两个人不同步干瞪眼的时间。 + /// + private void WarnIfTransformNotSynchronized() + { + if (!IsOwner) + return; + + if (GetComponent() == null) + { + Debug.LogWarning( + $"[{nameof(PlayerController)}] {name} 上没有 NetworkTransform,位置不会同步到其他客户端。" + + "请在 Player 预制体上加 NetworkTransform,并开启 Client Authoritative 与 Synchronize Position。", + this); + } } } }