统一使用新输入系统

This commit is contained in:
2026-09-20 17:09:58 +08:00
parent 38303b3743
commit 02265e75ca
14 changed files with 644 additions and 117 deletions
+1
View File
@@ -23,6 +23,7 @@ MonoBehaviour:
- {fileID: 201277550, guid: 26a567abbe21227428f5c3d309d1d73c, type: 3}
- {fileID: -8731164649411638372, guid: 687b16b0a8065d74fa4380e93a8a3d81, type: 3}
- {fileID: 4512293259955182956, guid: 44611030e61220d42ab7c37ba3c0ea92, type: 3}
- {fileID: 8499328425325690813, guid: 5d76bafdfa1223f40b69ba0a0c4de184, type: 3}
- {fileID: 8192566354860284824, guid: 6331b3542e64a564c81bc39cedf70c8d, type: 3}
- {fileID: 4512293259955182956, guid: f32d4c19de900e64cb73cedcb8ba6f70, type: 3}
- {fileID: 611616139817875448, guid: bf5f023b4017a5e41a9815ec5745df3d, type: 3}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 437148ba0acd7d84ea3f01d9a9472028
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,158 @@
{
"name": "PlayerControls",
"maps": [
{
"name": "Player",
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"actions": [
{
"name": "Move",
"type": "Value",
"id": "a1b2c3d4-0000-4000-8000-000000000002",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Look",
"type": "Value",
"id": "a1b2c3d4-0000-4000-8000-000000000003",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Jump",
"type": "Button",
"id": "a1b2c3d4-0000-4000-8000-000000000004",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Sprint",
"type": "Button",
"id": "a1b2c3d4-0000-4000-8000-000000000005",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Attack",
"type": "Button",
"id": "a1b2c3d4-0000-4000-8000-000000000006",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "WASD",
"id": "a1b2c3d4-0000-4000-8000-000000000007",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "a1b2c3d4-0000-4000-8000-000000000008",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "a1b2c3d4-0000-4000-8000-000000000009",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "a1b2c3d4-0000-4000-8000-00000000000a",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "a1b2c3d4-0000-4000-8000-00000000000b",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "a1b2c3d4-0000-4000-8000-00000000000c",
"path": "<Mouse>/delta",
"interactions": "",
"processors": "",
"groups": "",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "a1b2c3d4-0000-4000-8000-00000000000d",
"path": "<Keyboard>/space",
"interactions": "",
"processors": "",
"groups": "",
"action": "Jump",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "a1b2c3d4-0000-4000-8000-00000000000e",
"path": "<Keyboard>/leftShift",
"interactions": "",
"processors": "",
"groups": "",
"action": "Sprint",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "a1b2c3d4-0000-4000-8000-00000000000f",
"path": "<Mouse>/leftButton",
"interactions": "",
"processors": "",
"groups": "",
"action": "Attack",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": []
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 93d11597dae64cc4aa68018ccaa39195
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 0
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace:
+189
View File
@@ -0,0 +1,189 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &2655148725409191110
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 29487170300297484}
- component: {fileID: 3803348366860609995}
- component: {fileID: 969463025217515519}
- component: {fileID: 6765530772190315426}
- component: {fileID: 8499328425325690813}
- component: {fileID: 8072914292444380081}
m_Layer: 0
m_Name: ACube
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &29487170300297484
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2655148725409191110}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -13.66379, y: 2.13, z: 0.56447}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &3803348366860609995
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2655148725409191110}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &969463025217515519
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2655148725409191110}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!65 &6765530772190315426
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2655148725409191110}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &8499328425325690813
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2655148725409191110}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 26b716c41e9b56b4baafaf13a523ba2e, type: 3}
m_Name:
m_EditorClassIdentifier: FishNet.Runtime::FishNet.Object.NetworkObject
_localLevelOfDetailCalculationType: 0
_useLevelOfDetail: 0
_useRootLevelOfDetail: 1
NetworkObserver: {fileID: 0}
_enablePrediction: 0
_predictionType: 0
_localReconcileCorrectionType: 2
_graphicalObject: {fileID: 0}
_detachGraphicalObject: 0
_enableStateForwarding: 1
_networkTransform: {fileID: 0}
_ownerInterpolation: 1
_ownerSmoothedProperties: 255
_adaptiveInterpolation: 3
_spectatorSmoothedProperties: 255
_spectatorInterpolation: 2
_enableTeleport: 0
_teleportThreshold: 1
<PrefabId>k__BackingField: 10
<SpawnableCollectionId>k__BackingField: 0
<AssetPathHash>k__BackingField: 17143552955378212519
SceneId: 0
SerializedTransformProperties:
Position: {x: -13.66379, y: 2.13, z: 0.56447}
Rotation: {x: 0, y: 0, z: 0, w: 1}
Scale: {x: 1, y: 1, z: 1}
IsValid: 1
<IsNested>k__BackingField: 0
WasActiveDuringEdit: 0
WasActiveDuringEdit_Set1: 1
<ComponentIndex>k__BackingField: 0
<PredictedSpawn>k__BackingField: {fileID: 0}
<PredictedOwner>k__BackingField: {fileID: 0}
NetworkBehaviours:
- {fileID: 8072914292444380081}
InitializedParentNetworkBehaviour: {fileID: 0}
InitializedNestedNetworkObjects: []
RuntimeParentNetworkBehaviour: {fileID: 0}
RuntimeChildNetworkBehaviours: []
_isNetworked: 1
_isSpawnable: 1
_isGlobal: 0
_initializeOrder: 0
_preventDespawnOnDisconnect: 0
_defaultDespawnType: 0
_initializedTimestamp: -8584117148313536437
--- !u!114 &8072914292444380081
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2655148725409191110}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8a6a39c46bf52104ba8efe3100bce3f7, type: 3}
m_Name:
m_EditorClassIdentifier: FishNet.Runtime::FishNet.Object.EmptyNetworkBehaviour
_componentIndexCache: 0
_addedNetworkObject: {fileID: 0}
_networkObjectCache: {fileID: 8499328425325690813}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5d76bafdfa1223f40b69ba0a0c4de184
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+49 -1
View File
@@ -161,6 +161,8 @@ GameObject:
- component: {fileID: -8731164649411638372}
- component: {fileID: -1605824257395221361}
- component: {fileID: -1778631990423174556}
- component: {fileID: -1909282985868566644}
- component: {fileID: -2948519365385897836}
m_Layer: 0
m_Name: Player
m_TagString: Player
@@ -346,7 +348,7 @@ MonoBehaviour:
_initializeOrder: 0
_preventDespawnOnDisconnect: 0
_defaultDespawnType: 0
_initializedTimestamp: -8584117169363086877
_initializedTimestamp: -8584117164126792146
--- !u!114 &-1605824257395221361
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -408,3 +410,49 @@ MonoBehaviour:
X: 0
Y: 0
Z: 0
--- !u!114 &-1909282985868566644
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: 42ed262caf19a9040bf314a318cfecb6, type: 3}
m_Name:
m_EditorClassIdentifier: '::'
_componentIndexCache: 255
_addedNetworkObject: {fileID: -8731164649411638372}
_networkObjectCache: {fileID: 0}
cubePrefab: {fileID: 8499328425325690813, guid: 5d76bafdfa1223f40b69ba0a0c4de184, type: 3}
--- !u!114 &-2948519365385897836
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7344258574652498121}
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 62899f850307741f2a39c98a8b639597, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.PlayerInput
m_Actions: {fileID: -944628639613478452, guid: 93d11597dae64cc4aa68018ccaa39195, type: 3}
m_NotificationBehavior: 0
m_UIInputModule: {fileID: 0}
m_DeviceLostEvent:
m_PersistentCalls:
m_Calls: []
m_DeviceRegainedEvent:
m_PersistentCalls:
m_Calls: []
m_ControlsChangedEvent:
m_PersistentCalls:
m_Calls: []
m_ActionEvents: []
m_NeverAutoSwitchControlSchemes: 0
m_DefaultControlScheme:
m_DefaultActionMap: Player
m_SplitScreenIndex: -1
m_Camera: {fileID: 0}
+2 -68
View File
@@ -144,77 +144,12 @@ Transform:
m_GameObject: {fileID: 277106703}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -10.96996, y: 0.76613, z: -5.65952}
m_LocalPosition: {x: -18.34, y: 0.76613, z: -5.65952}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
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
@@ -569,6 +504,5 @@ SceneRoots:
- {fileID: 870123120}
- {fileID: 1617942571}
- {fileID: 1749431010}
- {fileID: 277106704}
- {fileID: 1541499318}
- {fileID: 383713395}
- {fileID: 277106704}
+32 -13
View File
@@ -6,12 +6,18 @@ namespace Agame
/// <summary>
/// 第一人称视角控制:鼠标横移带动 body 偏航,纵移带动相机俯仰。
///
/// 输入来自父节点(Player 根)上 PlayerInput 的 Look 动作。
///
/// 网络侧前提(重要):本组件挂在 PlayerCamera 上,而 PlayerCamera 只会被
/// <see cref="PlayerController"/> 为「本地拥有者」启用(见其 ApplyOwnership)。
/// 非拥有者客户端的相机是 disabled 的,因此这里不会执行,天然不需要再判所有权。
///
/// 反过来说:如果日后把 PlayerCamera 改成常驻启用,必须补上所有权判断,
/// 否则每个客户端都会用自己的鼠标去转动别人玩家的视角。
///
/// 关于光标:锁定/释放光标属于「窗口」层面,不是 gameplay 输入,
/// 所以刻意没有做成 Action —— 那样的话玩家一旦切到别的输入上下文
/// (比如打开菜单、禁用了 Player 这张 Map),连恢复光标的按键都收不到了。
/// </summary>
public class MouseLook : MonoBehaviour
{
@@ -27,11 +33,17 @@ namespace Agame
[Tooltip("被偏航(Horizontal)的躯体 Transform,通常是玩家根节点。")]
[SerializeField] private Transform characterBody;
private InputAction _lookAction;
private float _xRotation;
private bool _warnedMissingBody;
private void OnEnable()
{
// PlayerInput 挂在父节点(Player 根)上;相机自身可能是 disabled 的,
// 所以要 includeInactive 才能取到。
PlayerInput playerInput = GetComponentInParent<PlayerInput>(includeInactive: true);
_lookAction = PlayerInputNames.Resolve(playerInput, PlayerInputNames.Look, this);
// 跟随本对象的启用状态锁定光标:只有本地拥有者的相机会被启用,
// 所以光标锁定/释放自然就只在拥有者这一侧发生。
LockCursor(true);
@@ -45,23 +57,16 @@ namespace Agame
private void Update()
{
Mouse mouse = Mouse.current;
if (mouse == null)
return;
// Esc 释放光标,左键重新抓取(方便编辑器里切出去)。
Keyboard kb = Keyboard.current;
if (kb != null && kb.escapeKey.wasPressedThisFrame)
LockCursor(false);
if (mouse.leftButton.wasPressedThisFrame && Cursor.lockState != CursorLockMode.Locked)
LockCursor(true);
UpdateCursorLock();
if (Cursor.lockState != CursorLockMode.Locked)
return;
// 新输入系统下 delta 的单位是像素,不是轴值。
Vector2 delta = mouse.delta.ReadValue();
if (_lookAction == null)
return;
// Look 绑定的是 <Mouse>/delta,读出来的就是这一帧的鼠标位移(单位:像素)。
Vector2 delta = _lookAction.ReadValue<Vector2>();
// 横移:转动躯体
if (characterBody != null)
@@ -81,6 +86,20 @@ namespace Agame
transform.localRotation = Quaternion.Euler(_xRotation, 0f, 0f);
}
/// <summary>
/// Esc 释放光标,左键重新抓取(方便编辑器里切出去)。
/// </summary>
private void UpdateCursorLock()
{
Keyboard keyboard = Keyboard.current;
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
LockCursor(false);
Mouse mouse = Mouse.current;
if (mouse != null && mouse.leftButton.wasPressedThisFrame && Cursor.lockState != CursorLockMode.Locked)
LockCursor(true);
}
private void LockCursor(bool locked)
{
Cursor.lockState = locked ? CursorLockMode.Locked : CursorLockMode.None;
+56 -35
View File
@@ -7,18 +7,20 @@ using UnityEngine.InputSystem;
namespace Agame
{
/// <summary>
/// 玩家移动控制 —— FishNet 客户端授权模式。
/// 参考:Docs/联机方案选型.md §3.3,以及官方教程 "Moving Your Player Around"。
/// 玩家移动控制 —— FishNet 客户端授权 + Input System 的 PlayerInput / Actions 方案。
///
/// 职责边界:本组件只负责「读输入 + 驱动 CharacterController」。
/// 位置/旋转的同步交给 Player 预制体上的 NetworkTransform(Client Authoritative),
/// 这里不写任何 RPC / SyncVar。
/// 输入链路:
/// Assets/_Game/Input/PlayerControls.inputactions
/// → Player 根节点上的 PlayerInput 组件(仅本地拥有者启用)
/// → 本脚本在 Update 里读取 Move / Sprint / Jump
///
/// FishNet 4.7.3 下的三条硬约束:
/// 1. 必须继承 NetworkBehaviour 而非 MonoBehaviour,否则拿不到 IsOwner 与网络生命周期回调。
/// 2. IsOwner 禁止在 Awake / Start / OnStartServer / OnStartNetwork 里访问 ——
/// FishNet 代码生成器会直接抛编译错误,按拥有者初始化的逻辑要放在 OnStartClient。
/// 3. 只有拥有者能驱动自己的对象,少了这道判断就会去移动别人的玩家。
/// 本脚本同时兼任「输入与相机的所有权开关」,见 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
@@ -33,16 +35,36 @@ namespace Agame
[SerializeField, Min(0f)] private float jumpHeight = 1.5f;
[Header("Ownership")]
[Tooltip("仅本地拥有者需要启用的对象(玩家相机等)。非拥有者会被自动禁用,避免多相机抢占渲染。")]
[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()
@@ -59,47 +81,39 @@ namespace Agame
{
base.OnOwnershipClient(prevOwner);
// 所有权可能中途变更(例如将来的主机迁移),相机等要跟着切换。
// 所有权可能中途变更(例如将来的主机迁移),输入与相机要跟着切换。
ApplyOwnership();
}
private void Update()
{
// 只驱动本地拥有者,否则每个客户端都会去移动别人的玩家。
// 只驱动本地拥有者。
if (!IsOwner)
return;
Keyboard kb = Keyboard.current;
if (kb == null)
return;
Vector2 moveInput = _moveAction != null ? _moveAction.ReadValue<Vector2>() : Vector2.zero;
Vector3 moveDirection = BuildMoveDirection(moveInput);
float speed = kb.leftShiftKey.isPressed ? sprintSpeed : moveSpeed;
Vector3 moveDirection = ReadMoveDirection(kb);
bool isSprinting = _sprintAction != null && _sprintAction.IsPressed();
float speed = isSprinting ? sprintSpeed : moveSpeed;
_controller.Move(moveDirection * (speed * Time.deltaTime));
UpdateJumpAndGravity(kb);
bool jumpPressed = _jumpAction != null && _jumpAction.WasPressedThisFrame();
UpdateJumpAndGravity(jumpPressed);
}
/// <summary>
/// 读取 WASD 并转换为相对于角色自身朝向的方向(body 的偏航由 MouseLook 负责)。
/// 斜向输入做归一化,避免对角线移动比正向更快。
/// 把 Move 的二维输入转换为相对于角色自身朝向的三维方向
/// (body 的偏航由 MouseLook 负责)。斜向输入做归一化,避免对角线移动比正向更快。
/// </summary>
private Vector3 ReadMoveDirection(Keyboard kb)
private Vector3 BuildMoveDirection(Vector2 moveInput)
{
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;
Vector3 direction = transform.right * moveX + transform.forward * moveZ;
Vector3 direction = transform.right * moveInput.x + transform.forward * moveInput.y;
return Vector3.ClampMagnitude(direction, 1f);
}
private void UpdateJumpAndGravity(Keyboard kb)
private void UpdateJumpAndGravity(bool jumpPressed)
{
bool isGrounded = _controller.isGrounded;
@@ -107,7 +121,7 @@ namespace Agame
if (isGrounded && _verticalVelocity.y < 0f)
_verticalVelocity.y = -2f;
if (isGrounded && kb.spaceKey.wasPressedThisFrame)
if (isGrounded && jumpPressed)
{
// 由目标跳跃高度反推初速度:v = sqrt(2 * g * h)
_verticalVelocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
@@ -118,11 +132,18 @@ namespace Agame
}
/// <summary>
/// 按所有权开关"只属于本地玩家"的对象。最典型的是玩家相机:
/// 若每个客户端的相机都常驻启用,N 个玩家就会互相抢渲染。
/// 按所有权统一开关「只属于本地玩家」的东西:PlayerInput 与玩家相机。
///
/// 集中放在这里的原因:这两样都是「一个客户端只该有一份生效」。
/// 散到各脚本里各自 enable 容易漏,而且会出现顺序问题 ——
/// PlayerCamera 上的 MouseLook 在 OnEnable 里就要读 Look 动作,
/// 所以必须先开 PlayerInput、再启用相机,否则第一帧读不到值。
/// </summary>
private void ApplyOwnership()
{
if (_playerInput != null)
_playerInput.enabled = IsOwner;
if (ownerOnlyObjects == null)
return;
+54
View File
@@ -0,0 +1,54 @@
using FishNet.Object;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Serialization;
namespace Agame
{
/// <summary>
/// 按攻击键在当前位置生成一个方块,生成动作经 ServerRpc 在服务端执行。
///
/// 输入来自 Player 根节点上 PlayerInput 的 Attack 动作 —— 与 <see cref="PlayerController"/>
/// 共用同一个 PlayerInput(因此 PlayerInput 的启用/禁用也统一由 PlayerController 按所有权管理,
/// 这里只读动作,不再自己 enable 组件)。
/// </summary>
public class PlayerCubeCreator : NetworkBehaviour
{
[Tooltip("要生成的方块预制体。必须已登记进 FishNet 的可生成预制体列表(DefaultPrefabObjects)。")]
[FormerlySerializedAs("CubePrefab")]
[SerializeField] private NetworkObject cubePrefab;
private InputAction _attackAction;
private void Awake()
{
// Awake 中不得访问 IsOwner,这里只做纯本地缓存。
_attackAction = PlayerInputNames.Resolve(GetComponent<PlayerInput>(), PlayerInputNames.Attack, this);
}
private void Update()
{
// 只让拥有者发起攻击,否则每个客户端都会去指挥别人的玩家造方块。
if (!IsOwner)
return;
if (_attackAction != null && _attackAction.WasPressedThisFrame())
SpawnCube();
}
[ServerRpc]
private void SpawnCube()
{
if (cubePrefab == null)
{
Debug.LogError(
$"[{nameof(PlayerCubeCreator)}] {name} 的 Cube Prefab 没有指定,无法生成方块。",
this);
return;
}
NetworkObject obj = Instantiate(cubePrefab, transform.position, Quaternion.identity);
Spawn(obj); // NetworkBehaviour 对 ServerManager.Spawn 的快捷封装
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 42ed262caf19a9040bf314a318cfecb6
+70
View File
@@ -0,0 +1,70 @@
using UnityEngine;
using UnityEngine.InputSystem;
namespace Agame
{
/// <summary>
/// 输入资产 Assets/_Game/Input/PlayerControls.inputactions 的名称与取值入口。
///
/// 把 Action 名集中在这里,是为了避免 "Move"/"Look"/"Attack" 这类字符串散落在三个脚本里 ——
/// 将来在输入资产里改名,只需要动这一处。
///
/// 约定:三个玩家脚本一律从 PlayerInput 实例上取 Action,而不是各自序列化一个资产引用。
/// 因为 PlayerInput 可能为每个玩家实例化一份 Actions 副本,从实例上取才能拿到正确的那一份。
/// </summary>
public static class PlayerInputNames
{
/// <summary>输入资产的文件名,仅用于错误提示。</summary>
public const string AssetFileName = "PlayerControls.inputactions";
/// <summary>Action Map 名称,需与输入资产中的 Map 一致。</summary>
public const string ActionMap = "Player";
public const string Move = "Move";
public const string Look = "Look";
public const string Jump = "Jump";
public const string Sprint = "Sprint";
public const string Attack = "Attack";
/// <summary>
/// 从 PlayerInput 上解析指定 Action,解析失败时返回 null 并给出明确的错误信息。
/// 这里刻意用 LogError 而不是直接抛 NullReferenceException ——
/// "输入没反应但又不报错"是这类问题里最难排查的一种,必须留下痕迹。
/// </summary>
public static InputAction Resolve(PlayerInput playerInput, string actionName, Object context)
{
string ownerName = context != null ? context.name : "(未知对象)";
if (playerInput == null)
{
Debug.LogError(
$"[{nameof(PlayerInputNames)}] {ownerName} 上找不到 PlayerInput 组件,无法读取输入。" +
$"请在玩家预制体上加 PlayerInput 组件,并把 Actions 指向 {AssetFileName}。",
context);
return null;
}
InputActionAsset asset = playerInput.actions;
if (asset == null)
{
Debug.LogError(
$"[{nameof(PlayerInputNames)}] {ownerName} 的 PlayerInput 没有指定 Actions 资产," +
$"请把 {AssetFileName} 拖进 PlayerInput 的 Actions 槽。",
context);
return null;
}
InputAction action = asset.FindAction(actionName, throwIfNotFound: false);
if (action == null)
{
Debug.LogError(
$"[{nameof(PlayerInputNames)}] {AssetFileName} 里找不到名为 \"{actionName}\" 的动作," +
"请确认 Map 与 Action 名和脚本里的一致。",
context);
return null;
}
return action;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 379ed2c2823ca764982411eec577adce