单帧测试进行中
This commit is contained in:
parent
90f1832397
commit
1e90de11ce
|
|
@ -31,7 +31,6 @@ RectTransform:
|
|||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6308356813253026692}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
|
|
@ -107,7 +106,6 @@ RectTransform:
|
|||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6308356813253026692}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
|
|
@ -191,7 +189,6 @@ RectTransform:
|
|||
- {fileID: 6308356812888301747}
|
||||
- {fileID: 6308356813057413814}
|
||||
m_Father: {fileID: 6308356814245253662}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
|
|
@ -217,6 +214,7 @@ Canvas:
|
|||
m_SortingBucketNormalizedSize: 0
|
||||
m_VertexColorAlwaysGammaSpace: 0
|
||||
m_AdditionalShaderChannelsFlag: 0
|
||||
m_UpdateRectTransformForStandalone: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingOrder: 0
|
||||
m_TargetDisplay: 0
|
||||
|
|
@ -299,13 +297,13 @@ Transform:
|
|||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6308356813655391137}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.079317145, y: -0, z: -0, w: 0.9968494}
|
||||
m_LocalPosition: {x: -0.26855803, y: 1.55, z: -5.62}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6308356814245253662}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 9.099, y: 0, z: 0}
|
||||
--- !u!20 &6308356813655391139
|
||||
Camera:
|
||||
|
|
@ -321,9 +319,17 @@ Camera:
|
|||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_Iso: 200
|
||||
m_ShutterSpeed: 0.005
|
||||
m_Aperture: 16
|
||||
m_FocusDistance: 10
|
||||
m_FocalLength: 50
|
||||
m_BladeCount: 5
|
||||
m_Curvature: {x: 2, y: 11}
|
||||
m_BarrelClipping: 0.25
|
||||
m_Anamorphism: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_FocalLength: 50
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
|
|
@ -371,6 +377,7 @@ GameObject:
|
|||
- component: {fileID: 6308356814245253632}
|
||||
- component: {fileID: 6308356814245253634}
|
||||
- component: {fileID: 5069958635149219085}
|
||||
- component: {fileID: 8938569698484985372}
|
||||
- component: {fileID: -1362768914916555191}
|
||||
- component: {fileID: -8136683798838004576}
|
||||
m_Layer: 0
|
||||
|
|
@ -387,6 +394,7 @@ Transform:
|
|||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6308356814245253661}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
|
|
@ -395,7 +403,6 @@ Transform:
|
|||
- {fileID: 6308356813253026692}
|
||||
- {fileID: 6308356813655391140}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &6308356814245253633
|
||||
MeshFilter:
|
||||
|
|
@ -482,9 +489,26 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: db8117151f564304bae153aa55c0a960, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_sendInterval: 0.05
|
||||
_speed: 2
|
||||
_rigid: {fileID: -1362768914916555191}
|
||||
_inputComponent: {fileID: 8938569698484985372}
|
||||
_applyServerCorrection: 0
|
||||
_lerpRate: 0.1
|
||||
--- !u!114 &8938569698484985372
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6308356814245253661}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 76493c52bd37a4a4db167d10a4dd6369, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_playerId: 1234
|
||||
_sendInterval: 0.05
|
||||
_useNetwork: 1
|
||||
--- !u!54 &-1362768914916555191
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -492,10 +516,21 @@ Rigidbody:
|
|||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6308356814245253661}
|
||||
serializedVersion: 2
|
||||
serializedVersion: 4
|
||||
m_Mass: 2
|
||||
m_Drag: 5
|
||||
m_AngularDrag: 0
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ImplicitCom: 1
|
||||
m_ImplicitTensor: 1
|
||||
m_UseGravity: 1
|
||||
m_IsKinematic: 1
|
||||
m_Interpolate: 1
|
||||
|
|
@ -509,8 +544,16 @@ BoxCollider:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6308356814245253661}
|
||||
m_Material: {fileID: 0}
|
||||
m_IsTrigger: 0
|
||||
m_Enabled: 1
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -2394,6 +2394,7 @@ RectTransform:
|
|||
m_Children:
|
||||
- {fileID: 1979220485}
|
||||
- {fileID: 1013475929}
|
||||
- {fileID: 1638923373}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
|
|
@ -2420,6 +2421,9 @@ MonoBehaviour:
|
|||
_clientTickText: {fileID: 652355035}
|
||||
_correctionText: {fileID: 1413607043}
|
||||
_acknowledgedTickText: {fileID: 532753893}
|
||||
_testOneFrameButton: {fileID: 1997817542}
|
||||
_testFiveFramesButton: {fileID: 1923417043}
|
||||
_testIntermittentButton: {fileID: 1702927428}
|
||||
--- !u!1 &805112150
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -4508,6 +4512,85 @@ MeshFilter:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1360915757}
|
||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!1 &1411805405
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1411805406}
|
||||
- component: {fileID: 1411805408}
|
||||
- component: {fileID: 1411805407}
|
||||
m_Layer: 5
|
||||
m_Name: Text (Legacy)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1411805406
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1411805405}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1997817541}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1411805407
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1411805405}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_FontSize: 14
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 10
|
||||
m_MaxSize: 40
|
||||
m_Alignment: 4
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: OneFrame
|
||||
--- !u!222 &1411805408
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1411805405}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1413607041
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -5248,6 +5331,44 @@ Transform:
|
|||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1638923372
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1638923373}
|
||||
m_Layer: 5
|
||||
m_Name: Buttons
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1638923373
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1638923372}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1997817541}
|
||||
- {fileID: 1923417042}
|
||||
- {fileID: 1702927427}
|
||||
m_Father: {fileID: 789249236}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &1643006720
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -5565,6 +5686,139 @@ CanvasRenderer:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1676055458}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1702927426
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1702927427}
|
||||
- component: {fileID: 1702927430}
|
||||
- component: {fileID: 1702927429}
|
||||
- component: {fileID: 1702927428}
|
||||
m_Layer: 5
|
||||
m_Name: Intermittent
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1702927427
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1702927426}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1975840260}
|
||||
m_Father: {fileID: 1638923373}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -578, y: 54}
|
||||
m_SizeDelta: {x: 160, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1702927428
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1702927426}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 1702927429}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 789249231}
|
||||
m_TargetAssemblyTypeName:
|
||||
m_MethodName:
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName:
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &1702927429
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1702927426}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!222 &1702927430
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1702927426}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1817537069
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -5707,6 +5961,139 @@ Transform:
|
|||
- {fileID: 1993386580}
|
||||
m_Father: {fileID: 1186082400}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1923417041
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1923417042}
|
||||
- component: {fileID: 1923417045}
|
||||
- component: {fileID: 1923417044}
|
||||
- component: {fileID: 1923417043}
|
||||
m_Layer: 5
|
||||
m_Name: FiveFrame
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1923417042
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1923417041}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1940656813}
|
||||
m_Father: {fileID: 1638923373}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -578, y: 119}
|
||||
m_SizeDelta: {x: 160, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1923417043
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1923417041}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 1923417044}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 789249231}
|
||||
m_TargetAssemblyTypeName:
|
||||
m_MethodName:
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName:
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &1923417044
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1923417041}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!222 &1923417045
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1923417041}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1937973361
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -5786,6 +6173,164 @@ CanvasRenderer:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1937973361}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1940656812
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1940656813}
|
||||
- component: {fileID: 1940656815}
|
||||
- component: {fileID: 1940656814}
|
||||
m_Layer: 5
|
||||
m_Name: Text (Legacy)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1940656813
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1940656812}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1923417042}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1940656814
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1940656812}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_FontSize: 14
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 10
|
||||
m_MaxSize: 40
|
||||
m_Alignment: 4
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: FiveFrame
|
||||
--- !u!222 &1940656815
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1940656812}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1975840259
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1975840260}
|
||||
- component: {fileID: 1975840262}
|
||||
- component: {fileID: 1975840261}
|
||||
m_Layer: 5
|
||||
m_Name: Text (Legacy)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1975840260
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1975840259}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1702927427}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1975840261
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1975840259}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_FontSize: 14
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 10
|
||||
m_MaxSize: 40
|
||||
m_Alignment: 4
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: Intermittent
|
||||
--- !u!222 &1975840262
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1975840259}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1979220484
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -5997,6 +6542,139 @@ MeshFilter:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1993386579}
|
||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!1 &1997817540
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1997817541}
|
||||
- component: {fileID: 1997817544}
|
||||
- component: {fileID: 1997817543}
|
||||
- component: {fileID: 1997817542}
|
||||
m_Layer: 5
|
||||
m_Name: OneFrame
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1997817541
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1997817540}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1411805406}
|
||||
m_Father: {fileID: 1638923373}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -578, y: 176}
|
||||
m_SizeDelta: {x: 160, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1997817542
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1997817540}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 1997817543}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 789249231}
|
||||
m_TargetAssemblyTypeName:
|
||||
m_MethodName:
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName:
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &1997817543
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1997817540}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!222 &1997817544
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1997817540}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &2021317914
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public static class ClientGameplayInputFlow
|
||||
{
|
||||
public static bool HasPlanarInput(Vector3 input)
|
||||
{
|
||||
return new Vector2(input.x, input.z).sqrMagnitude > 0f;
|
||||
}
|
||||
|
||||
public static bool TryCreateMoveInput(string playerId, long tick, Vector3 input, bool stopMessagePending,
|
||||
out MoveInput message)
|
||||
{
|
||||
if (!HasPlanarInput(input) && !stopMessagePending)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = new MoveInput
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Tick = tick,
|
||||
TurnInput = -input.x,
|
||||
ThrottleInput = input.z
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryCreateShootInput(
|
||||
string playerId,
|
||||
long tick,
|
||||
bool fireTriggered,
|
||||
Vector3 aimDirection,
|
||||
out ShootInput message,
|
||||
string targetId = "")
|
||||
{
|
||||
if (!fireTriggered)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = CreateShootInput(playerId, tick, aimDirection, targetId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "")
|
||||
{
|
||||
var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z);
|
||||
if (planarDirection.sqrMagnitude <= 0f)
|
||||
{
|
||||
planarDirection = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
planarDirection.Normalize();
|
||||
}
|
||||
|
||||
return new ShootInput
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Tick = tick,
|
||||
DirX = planarDirection.x,
|
||||
DirY = planarDirection.z,
|
||||
TargetId = targetId ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static void SendShootInput(
|
||||
MessageManager messageManager,
|
||||
string playerId,
|
||||
long tick,
|
||||
Vector3 aimDirection,
|
||||
string targetId = "")
|
||||
{
|
||||
if (messageManager == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(messageManager));
|
||||
}
|
||||
|
||||
SendShootInput(messageManager, CreateShootInput(playerId, tick, aimDirection, targetId));
|
||||
}
|
||||
|
||||
public static void SendShootInput(MessageManager messageManager, ShootInput message)
|
||||
{
|
||||
if (messageManager == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(messageManager));
|
||||
}
|
||||
|
||||
if (message == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(message));
|
||||
}
|
||||
|
||||
messageManager.SendMessage(message, MessageType.ShootInput);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e3a66b8917bed9648a9d99ee35525e6e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Network.Defines;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
/// <summary>
|
||||
/// 输入源接口,用于解耦输入捕获
|
||||
/// </summary>
|
||||
public interface IInputSource
|
||||
{
|
||||
Vector3 GetPlanarInput();
|
||||
bool ConsumeShootInput();
|
||||
Vector3 GetAimDirection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 真实的 Unity 输入源
|
||||
/// </summary>
|
||||
public class UnityInputSource : IInputSource
|
||||
{
|
||||
private readonly Transform _cameraTransform;
|
||||
|
||||
public UnityInputSource(Transform cameraTransform)
|
||||
{
|
||||
_cameraTransform = cameraTransform;
|
||||
}
|
||||
|
||||
public Vector3 GetPlanarInput()
|
||||
{
|
||||
return new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
|
||||
}
|
||||
|
||||
public bool ConsumeShootInput()
|
||||
{
|
||||
return Input.GetMouseButtonDown(0);
|
||||
}
|
||||
|
||||
public Vector3 GetAimDirection()
|
||||
{
|
||||
if (_cameraTransform != null)
|
||||
{
|
||||
return _cameraTransform.forward;
|
||||
}
|
||||
return Vector3.forward;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟输入源(测试用),提供预设的输入序列
|
||||
/// </summary>
|
||||
public class SimulatedInputSource : IInputSource
|
||||
{
|
||||
private readonly (float turn, float throttle)[] _inputSequence;
|
||||
private int _index;
|
||||
private Vector3 _lastAimDirection = Vector3.forward;
|
||||
private bool _shootTriggered;
|
||||
|
||||
public SimulatedInputSource((float turn, float throttle)[] sequence)
|
||||
{
|
||||
_inputSequence = sequence;
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
public Vector3 GetPlanarInput()
|
||||
{
|
||||
if (_index >= _inputSequence.Length)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
var (turn, throttle) = _inputSequence[_index];
|
||||
return new Vector3(turn, 0f, throttle);
|
||||
}
|
||||
|
||||
public bool ConsumeShootInput()
|
||||
{
|
||||
if (_shootTriggered)
|
||||
{
|
||||
_shootTriggered = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Vector3 GetAimDirection()
|
||||
{
|
||||
return _lastAimDirection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推进到下一个输入
|
||||
/// </summary>
|
||||
public void Advance()
|
||||
{
|
||||
if (_index < _inputSequence.Length)
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否还有更多输入
|
||||
/// </summary>
|
||||
public bool HasMore => _index < _inputSequence.Length;
|
||||
|
||||
/// <summary>
|
||||
/// 设置射击触发(下次 ConsumeShootInput 返回 true)
|
||||
/// </summary>
|
||||
public void SetShootTriggered()
|
||||
{
|
||||
_shootTriggered = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置瞄准方向
|
||||
/// </summary>
|
||||
public void SetAimDirection(Vector3 direction)
|
||||
{
|
||||
_lastAimDirection = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前输入索引
|
||||
/// </summary>
|
||||
public int CurrentIndex => _index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输入组件,负责从 IInputSource 获取输入、发送 MoveInput 到服务器、管理 tick
|
||||
/// Update() 是唯一调用 SendMoveInput / SendShootInput 的地方
|
||||
/// </summary>
|
||||
public class InputComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string _playerId = "player";
|
||||
[SerializeField] private float _sendInterval = 0.05f; // 50ms 发送间隔
|
||||
|
||||
private IInputSource _inputSource;
|
||||
private Vector3 _currentInput;
|
||||
private Vector3 _lastAimDirection = Vector3.forward;
|
||||
private float _lastSendTime;
|
||||
private bool _stopMessagePending;
|
||||
private bool _wasMovingLastFrame;
|
||||
private long _tick;
|
||||
|
||||
public event System.Action<MoveInput> OnMoveInputCreated;
|
||||
public event System.Action<ShootInput> OnShootInputCreated;
|
||||
|
||||
public long CurrentTick => _tick;
|
||||
|
||||
/// <summary>
|
||||
/// 设置玩家 ID(由 MovementComponent.Init 调用)
|
||||
/// </summary>
|
||||
public void InjectPlayerId(string playerId)
|
||||
{
|
||||
_playerId = playerId;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
var camera = Camera.main;
|
||||
_inputSource = new UnityInputSource(camera?.transform);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置自定义输入源(用于替换默认的 UnityInputSource)
|
||||
/// </summary>
|
||||
public void SetInputSource(IInputSource source)
|
||||
{
|
||||
_inputSource = source ?? throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前输入源(用于测试)
|
||||
/// </summary>
|
||||
public IInputSource GetInputSource()
|
||||
{
|
||||
return _inputSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置 tick(用于测试)
|
||||
/// </summary>
|
||||
public void ResetTick(long tick = 0)
|
||||
{
|
||||
_tick = tick;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 从输入源获取输入
|
||||
_currentInput = _inputSource?.GetPlanarInput() ?? Vector3.zero;
|
||||
|
||||
// 检测移动状态变化
|
||||
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_currentInput);
|
||||
if (hasMovement)
|
||||
{
|
||||
_stopMessagePending = false;
|
||||
}
|
||||
else if (_wasMovingLastFrame)
|
||||
{
|
||||
_stopMessagePending = true;
|
||||
}
|
||||
|
||||
_wasMovingLastFrame = hasMovement;
|
||||
|
||||
// 处理射击输入
|
||||
var shootInput = GetShootInput();
|
||||
if (shootInput != null && NetworkManager.Instance != null)
|
||||
{
|
||||
NetworkManager.Instance.SendShootInput(shootInput);
|
||||
OnShootInputCreated?.Invoke(shootInput);
|
||||
}
|
||||
|
||||
// 定期发送移动输入
|
||||
if (Time.time - _lastSendTime > _sendInterval)
|
||||
{
|
||||
SendMoveInput();
|
||||
}
|
||||
}
|
||||
|
||||
private void SendMoveInput()
|
||||
{
|
||||
if (!ClientGameplayInputFlow.TryCreateMoveInput(_playerId, _tick, _currentInput,
|
||||
_stopMessagePending, out var moveInput))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NetworkManager.Instance != null)
|
||||
{
|
||||
NetworkManager.Instance.SendMoveInput(moveInput);
|
||||
}
|
||||
|
||||
OnMoveInputCreated?.Invoke(moveInput);
|
||||
_stopMessagePending = false;
|
||||
_lastSendTime = Time.time;
|
||||
_tick++;
|
||||
}
|
||||
|
||||
private ShootInput GetShootInput()
|
||||
{
|
||||
var shootTriggered = _inputSource?.ConsumeShootInput() ?? false;
|
||||
var aimDirection = _inputSource?.GetAimDirection() ?? Vector3.forward;
|
||||
|
||||
if (!shootTriggered)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var planarForward = Vector3.ProjectOnPlane(aimDirection, Vector3.up);
|
||||
if (ClientGameplayInputFlow.HasPlanarInput(planarForward))
|
||||
{
|
||||
_lastAimDirection = planarForward;
|
||||
}
|
||||
|
||||
return ClientGameplayInputFlow.CreateShootInput(_playerId, _tick, _lastAimDirection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 76493c52bd37a4a4db167d10a4dd6369
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -4,116 +4,23 @@ using Network.NetworkApplication;
|
|||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public static class ClientGameplayInputFlow
|
||||
{
|
||||
public static bool HasPlanarInput(Vector3 input)
|
||||
{
|
||||
return new Vector2(input.x, input.z).sqrMagnitude > 0f;
|
||||
}
|
||||
|
||||
public static bool TryCreateMoveInput(string playerId, long tick, Vector3 input, bool stopMessagePending, out MoveInput message)
|
||||
{
|
||||
if (!HasPlanarInput(input) && !stopMessagePending)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = new MoveInput
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Tick = tick,
|
||||
TurnInput = -input.x,
|
||||
ThrottleInput = input.z
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryCreateShootInput(
|
||||
string playerId,
|
||||
long tick,
|
||||
bool fireTriggered,
|
||||
Vector3 aimDirection,
|
||||
out ShootInput message,
|
||||
string targetId = "")
|
||||
{
|
||||
if (!fireTriggered)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = CreateShootInput(playerId, tick, aimDirection, targetId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "")
|
||||
{
|
||||
var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z);
|
||||
if (planarDirection.sqrMagnitude <= 0f)
|
||||
{
|
||||
planarDirection = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
planarDirection.Normalize();
|
||||
}
|
||||
|
||||
return new ShootInput
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Tick = tick,
|
||||
DirX = planarDirection.x,
|
||||
DirY = planarDirection.z,
|
||||
TargetId = targetId ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static void SendShootInput(
|
||||
MessageManager messageManager,
|
||||
string playerId,
|
||||
long tick,
|
||||
Vector3 aimDirection,
|
||||
string targetId = "")
|
||||
{
|
||||
if (messageManager == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(messageManager));
|
||||
}
|
||||
|
||||
SendShootInput(messageManager, CreateShootInput(playerId, tick, aimDirection, targetId));
|
||||
}
|
||||
|
||||
public static void SendShootInput(MessageManager messageManager, ShootInput message)
|
||||
{
|
||||
if (messageManager == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(messageManager));
|
||||
}
|
||||
|
||||
if (message == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(message));
|
||||
}
|
||||
|
||||
messageManager.SendMessage(message, MessageType.ShootInput);
|
||||
}
|
||||
}
|
||||
|
||||
public class MovementComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float _sendInterval = 0.05f;
|
||||
[SerializeField] private int _speed = 2;
|
||||
[SerializeField] private Rigidbody _rigid;
|
||||
[SerializeField] private InputComponent _inputComponent;
|
||||
|
||||
private Player _master;
|
||||
private const float TurnSpeedDegreesPerSecond = 180f;
|
||||
|
||||
// 测试时设为 false,可接收服务器状态日志但不应用校正
|
||||
[SerializeField] private bool _applyServerCorrection = true;
|
||||
private const float UnityYawOffsetDegrees = 90f;
|
||||
|
||||
// Server authoritative movement cadence used for replay substepping.
|
||||
// This matches ServerAuthoritativeMovementConfiguration.SimulationInterval (50ms).
|
||||
private const float kServerSimulationStepSeconds = 0.05f;
|
||||
|
||||
private int _speed = 2;
|
||||
[SerializeField] private Rigidbody _rigid;
|
||||
private float _lastSendTime = 0;
|
||||
private bool _isControlled = false;
|
||||
|
||||
private Vector3 _serverPosition;
|
||||
|
|
@ -124,14 +31,12 @@ public class MovementComponent : MonoBehaviour
|
|||
public long Tick { get; private set; } = 0;
|
||||
private long _startTickOffset = 0;
|
||||
private long _currentTickOffset = 0;
|
||||
private float _simulationAccumulator = 0f;
|
||||
private readonly ClientPredictionBuffer _predictionBuffer = new ClientPredictionBuffer();
|
||||
|
||||
private readonly RemotePlayerSnapshotInterpolator _remoteSnapshotInterpolator = new();
|
||||
[SerializeField] private float _lerpRate = 0.1f;
|
||||
private Vector3 _cachedMoveInput;
|
||||
private Vector3 _lastAimDirection = Vector3.forward;
|
||||
private bool _wasMovingLastFrame;
|
||||
private bool _stopMessagePending;
|
||||
|
||||
public void Init(bool isControlled, Player master, ClientMovementBootstrap bootstrap)
|
||||
{
|
||||
|
|
@ -148,47 +53,58 @@ public class MovementComponent : MonoBehaviour
|
|||
_rigid.isKinematic = !isControlled;
|
||||
_rigid.velocity = Vector3.zero;
|
||||
_rigid.angularVelocity = Vector3.zero;
|
||||
if (serverTick != 0 && _isControlled && MainUI.Instance != null) MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
||||
|
||||
// 设置 InputComponent 的 playerId
|
||||
if (_inputComponent != null)
|
||||
{
|
||||
_inputComponent.InjectPlayerId(master.PlayerId);
|
||||
}
|
||||
|
||||
if (serverTick != 0 && _isControlled && MainUI.Instance != null)
|
||||
MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isControlled)
|
||||
{
|
||||
_cachedMoveInput = CaptureMovement();
|
||||
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput);
|
||||
if (hasMovement)
|
||||
if (_inputComponent != null)
|
||||
{
|
||||
_stopMessagePending = false;
|
||||
MainUI.Instance.OnClientTickChanged(_inputComponent.CurrentTick);
|
||||
}
|
||||
}
|
||||
else if (_wasMovingLastFrame)
|
||||
{
|
||||
_stopMessagePending = true;
|
||||
}
|
||||
|
||||
_wasMovingLastFrame = hasMovement;
|
||||
|
||||
var shootInput = CaptureShootInput();
|
||||
if (shootInput != null)
|
||||
private void Start()
|
||||
{
|
||||
NetworkManager.Instance.SendShootInput(shootInput);
|
||||
// 订阅 InputComponent 的事件来记录预测输入
|
||||
if (_inputComponent != null)
|
||||
{
|
||||
_inputComponent.OnMoveInputCreated += HandleMoveInputCreated;
|
||||
}
|
||||
}
|
||||
|
||||
if (Time.time - _lastSendTime > _sendInterval)
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (ClientGameplayInputFlow.TryCreateMoveInput(_master.PlayerId, Tick, _cachedMoveInput, _stopMessagePending, out var moveInput))
|
||||
if (_inputComponent != null)
|
||||
{
|
||||
NetworkManager.Instance.SendMoveInput(moveInput);
|
||||
_inputComponent.OnMoveInputCreated -= HandleMoveInputCreated;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMoveInputCreated(MoveInput moveInput)
|
||||
{
|
||||
// 记录到预测缓冲区,用于后续的服务器状态校正和回放
|
||||
_predictionBuffer.Record(moveInput);
|
||||
_stopMessagePending = false;
|
||||
}
|
||||
|
||||
_lastSendTime = Time.time;
|
||||
Tick++;
|
||||
|
||||
MainUI.Instance.OnClientTickChanged(Tick);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 测试用:设置是否应用服务器状态校正(默认 true)
|
||||
/// 设为 false 时只打印服务器状态日志,不影响本地位置
|
||||
/// </summary>
|
||||
public void SetApplyServerCorrection(bool apply)
|
||||
{
|
||||
_applyServerCorrection = apply;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
|
|
@ -206,10 +122,17 @@ public class MovementComponent : MonoBehaviour
|
|||
_hasServerState = false;
|
||||
}
|
||||
|
||||
Simulate(_cachedMoveInput);
|
||||
// Use actual elapsed wall-clock time since last authoritative state,
|
||||
// decoupled from FixedUpdate cadence, to match server's 20Hz cadence.
|
||||
_predictionBuffer.AccumulateWithElapsedTime(Time.time - _predictionBuffer.LastAuthoritativeStateTime);
|
||||
// 累积时间,按服务端 50ms 步长进行模拟
|
||||
_simulationAccumulator += Time.fixedDeltaTime;
|
||||
while (_simulationAccumulator >= kServerSimulationStepSeconds)
|
||||
{
|
||||
// 使用最近发送的 MoveInput(来自 predictionBuffer)而非实时输入,
|
||||
// 确保客户端与服务端的输入时序一致
|
||||
Simulate(GetLatestPredictedInput());
|
||||
_simulationAccumulator -= kServerSimulationStepSeconds;
|
||||
}
|
||||
|
||||
// 注意:模拟时间现在在 Simulate() 内部通过 AccumulateLatest 累加
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -238,7 +161,8 @@ public class MovementComponent : MonoBehaviour
|
|||
predictedRotation,
|
||||
snapshot.Position,
|
||||
snapshot.RotationQuaternion,
|
||||
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond, snapDistanceMultiplier: 5f),
|
||||
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond,
|
||||
snapDistanceMultiplier: 5f),
|
||||
_activeVisualCorrection);
|
||||
|
||||
_activeVisualCorrection = correction.NextState;
|
||||
|
|
@ -246,7 +170,17 @@ public class MovementComponent : MonoBehaviour
|
|||
_rigid.rotation = correction.Rotation;
|
||||
_rigid.velocity = correction.UsedHardSnap ? snapshot.Velocity : Vector3.zero;
|
||||
_rigid.angularVelocity = Vector3.zero;
|
||||
ReplayPendingInputs(replayInputs);
|
||||
|
||||
// 位置已被校正到服务器位置,不需要再 ReplayPendingInputs
|
||||
// 因为 pendingInputs 中的 SimulatedDurationSeconds 是累积的模拟时间,
|
||||
// 如果用来 replay 会导致多余的移动。清空 pendingInputs 让客户端从校正位置重新开始
|
||||
if (replayInputs.Count > 0)
|
||||
{
|
||||
_predictionBuffer.ClearPendingInputs();
|
||||
}
|
||||
|
||||
// 清零 accumulator 防止 FixedUpdate 中再次 Simulate 导致重复移动
|
||||
_simulationAccumulator = 0f;
|
||||
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
|
|
@ -259,23 +193,6 @@ public class MovementComponent : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
private Vector3 CaptureMovement()
|
||||
{
|
||||
return new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
|
||||
}
|
||||
|
||||
private ShootInput CaptureShootInput()
|
||||
{
|
||||
return ClientGameplayInputFlow.TryCreateShootInput(
|
||||
_master.PlayerId,
|
||||
Tick,
|
||||
Input.GetMouseButtonDown(0),
|
||||
ResolveAimDirection(),
|
||||
out var shootInput)
|
||||
? shootInput
|
||||
: null;
|
||||
}
|
||||
|
||||
private Vector3 ResolveAimDirection()
|
||||
{
|
||||
var planarForward = Vector3.ProjectOnPlane(_rigid.transform.forward, Vector3.up);
|
||||
|
|
@ -285,18 +202,29 @@ public class MovementComponent : MonoBehaviour
|
|||
return planarForward;
|
||||
}
|
||||
|
||||
return ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection) ? _lastAimDirection : ResolveHeadingForward(UnityYawToHeading(_rigid.rotation.eulerAngles.y));
|
||||
return ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection)
|
||||
? _lastAimDirection
|
||||
: ResolveHeadingForward(UnityYawToHeading(_rigid.rotation.eulerAngles.y));
|
||||
}
|
||||
|
||||
private void Simulate(Vector3 input)
|
||||
{
|
||||
ApplyTankMovement(-input.x, input.z, Time.fixedDeltaTime);
|
||||
ApplyTankMovement(-input.x, input.z, kServerSimulationStepSeconds);
|
||||
|
||||
// 每次 Simulate 后累加模拟时间(用于 Reconcile 时的重放)
|
||||
_predictionBuffer.AccumulateLatest(kServerSimulationStepSeconds);
|
||||
|
||||
if (_isControlled)
|
||||
{
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_rigid.position);
|
||||
}
|
||||
|
||||
// 打印客户端当前状态,用于与服务端状态对比
|
||||
Debug.Log($"[ClientState] Tick={Tick} " +
|
||||
$"Pos=({_rigid.position.x:F3}, {_rigid.position.y:F3}, {_rigid.position.z:F3}) " +
|
||||
$"Rot={_rigid.rotation.eulerAngles.y:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -305,8 +233,33 @@ public class MovementComponent : MonoBehaviour
|
|||
if (_isControlled)
|
||||
{
|
||||
_lastAuthoritativeState = snapshot;
|
||||
|
||||
// 打印服务端状态,用于与客户端计算结果对比
|
||||
Debug.Log($"[ServerState] Tick={snapshot.SourceState.Tick} " +
|
||||
$"Pos=({snapshot.SourceState.Position.X:F3}, {snapshot.SourceState.Position.Y:F3}, {snapshot.SourceState.Position.Z:F3}) " +
|
||||
$"Rot={snapshot.SourceState.Rotation:F2} " +
|
||||
$"Vel=({snapshot.SourceState.Velocity.X:F3}, {snapshot.SourceState.Velocity.Y:F3}, {snapshot.SourceState.Velocity.Z:F3}) " +
|
||||
$"AckTick={snapshot.AcknowledgedMoveTick}");
|
||||
|
||||
// 清理已确认的旧输入,确保客户端使用正确的(已确认的)输入
|
||||
var pendingBefore = _predictionBuffer.PendingInputs.Count;
|
||||
_predictionBuffer.PruneAcknowledgedInputs(snapshot.AcknowledgedMoveTick);
|
||||
var pendingAfter = _predictionBuffer.PendingInputs.Count;
|
||||
Debug.Log(
|
||||
$"[Prune] AckTick={snapshot.AcknowledgedMoveTick} removed {pendingBefore - pendingAfter}/{pendingBefore} inputs, remaining={pendingAfter}");
|
||||
|
||||
// 收到服务器状态后,必须清空 pendingInputs
|
||||
// 因为 pendingInputs 中的 SimulatedDurationSeconds 是累积的模拟时间,
|
||||
// 如果不清理,客户端会继续用这些输入移动(测试模式下位置不被服务器校正)
|
||||
_predictionBuffer.ClearPendingInputs();
|
||||
_simulationAccumulator = 0f;
|
||||
|
||||
// 只有开启校正时才设置 _hasServerState,否则只打印日志不应用
|
||||
if (_applyServerCorrection)
|
||||
{
|
||||
_hasServerState = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastAuthoritativeState = snapshot;
|
||||
|
|
@ -326,6 +279,28 @@ public class MovementComponent : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近发送的 MoveInput,用于与服务器输入时序对齐。
|
||||
/// 如果没有记录的输入,返回零向量(停止状态)。
|
||||
/// </summary>
|
||||
private Vector3 GetLatestPredictedInput()
|
||||
{
|
||||
var pending = _predictionBuffer.PendingInputs;
|
||||
if (pending.Count == 0)
|
||||
{
|
||||
Debug.Log("[MoveInput] No pending inputs, using zero (stop)");
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
var latest = pending[^1];
|
||||
Debug.Log(
|
||||
$"[MoveInput] Using tick={latest.Input.Tick} TurnInput={latest.Input.TurnInput} ThrottleInput={latest.Input.ThrottleInput} ({pending.Count} pending)");
|
||||
// MoveInput 的 TurnInput/ThrottleInput 转回 Unity 的 x/z 格式
|
||||
// 注意 TurnInput 在 MoveInput 里是正数=右,正数=-input.x=左(需要取反)
|
||||
// ThrottleInput 在 MoveInput 里正数=前进,正数=input.z=前
|
||||
return new Vector3(-latest.Input.TurnInput, 0f, latest.Input.ThrottleInput);
|
||||
}
|
||||
|
||||
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
|
||||
{
|
||||
foreach (var replayInput in replayInputs)
|
||||
|
|
@ -363,13 +338,19 @@ public class MovementComponent : MonoBehaviour
|
|||
|
||||
var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
|
||||
var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f);
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(_rigid.rotation.eulerAngles.y) + (clampedTurnInput * TurnSpeedDegreesPerSecond * deltaTime));
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(_rigid.rotation.eulerAngles.y) +
|
||||
(clampedTurnInput * TurnSpeedDegreesPerSecond * deltaTime));
|
||||
_rigid.rotation = Quaternion.Euler(0f, HeadingToUnityYaw(heading), 0f);
|
||||
|
||||
var forward = ResolveHeadingForward(heading);
|
||||
var velocity = forward * (clampedThrottleInput * _speed);
|
||||
_rigid.velocity = velocity;
|
||||
_rigid.position += velocity * deltaTime;
|
||||
|
||||
// 调试日志:打印每步计算细节
|
||||
Debug.Log($"[MoveStep] _speed={_speed} deltaTime={deltaTime:F4} throttle={clampedThrottleInput} " +
|
||||
$"heading={heading:F2} velocity=({velocity.x:F3}, {velocity.y:F3}, {velocity.z:F3}) " +
|
||||
$"pos=({_rigid.position.x:F3}, {_rigid.position.y:F3}, {_rigid.position.z:F3})");
|
||||
}
|
||||
|
||||
private static Vector3 ResolveHeadingForward(float headingDegrees)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,15 @@ namespace Network.NetworkApplication
|
|||
|
||||
public IReadOnlyList<PredictedMoveStep> PendingInputs => pendingInputs;
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有 pending inputs。
|
||||
/// 用于 Reconcile 后清理已重放的输入,避免它们的时间被继续累积。
|
||||
/// </summary>
|
||||
public void ClearPendingInputs()
|
||||
{
|
||||
pendingInputs.Clear();
|
||||
}
|
||||
|
||||
public void Record(MoveInput input)
|
||||
{
|
||||
if (input == null)
|
||||
|
|
@ -63,7 +72,8 @@ namespace Network.NetworkApplication
|
|||
}
|
||||
|
||||
var latest = pendingInputs[^1];
|
||||
pendingInputs[^1] = new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + simulatedDurationSeconds);
|
||||
pendingInputs[^1] =
|
||||
new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + simulatedDurationSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -79,10 +89,12 @@ namespace Network.NetworkApplication
|
|||
}
|
||||
|
||||
var latest = pendingInputs[^1];
|
||||
pendingInputs[^1] = new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + elapsedSinceLastState);
|
||||
pendingInputs[^1] =
|
||||
new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + elapsedSinceLastState);
|
||||
}
|
||||
|
||||
public bool TryApplyAuthoritativeState(PlayerState state, float currentTime, out IReadOnlyList<PredictedMoveStep> replayInputs)
|
||||
public bool TryApplyAuthoritativeState(PlayerState state, float currentTime,
|
||||
out IReadOnlyList<PredictedMoveStep> replayInputs)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
|
|
@ -105,5 +117,20 @@ namespace Network.NetworkApplication
|
|||
_lastAuthoritativeStateTime = currentTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只清除已确认的旧输入,不触发 replay,不更新 LastAuthoritativeTick。
|
||||
/// 用于在校正被禁用时,保持 predictionBuffer 的输入与服务端同步。
|
||||
/// </summary>
|
||||
public void PruneAcknowledgedInputs(long acknowledgedMoveTick)
|
||||
{
|
||||
if (acknowledgedMoveTick <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pendingInputs.RemoveAll(input => input.Input.Tick <= acknowledgedMoveTick);
|
||||
LastAcknowledgedMoveTick = acknowledgedMoveTick;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ namespace Network.NetworkHost
|
|||
}
|
||||
|
||||
if (!movementCoordinator.TryGetState(acceptedPeer, out attackerState) &&
|
||||
!movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, out attackerState))
|
||||
!movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, null, out attackerState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ namespace Network.NetworkHost
|
|||
|
||||
public TimeSpan SimulationInterval => configuration.SimulationInterval;
|
||||
|
||||
public float MoveSpeed => configuration.MoveSpeed;
|
||||
|
||||
public IReadOnlyList<ServerAuthoritativeMovementState> States
|
||||
{
|
||||
get
|
||||
|
|
@ -47,7 +49,7 @@ namespace Network.NetworkHost
|
|||
}
|
||||
}
|
||||
|
||||
public bool EnsureState(IPEndPoint remoteEndPoint, string playerId, out ServerAuthoritativeMovementState state)
|
||||
public bool EnsureState(IPEndPoint remoteEndPoint, string playerId, float? speed, out ServerAuthoritativeMovementState state)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
|
|
@ -73,14 +75,21 @@ namespace Network.NetworkHost
|
|||
return false;
|
||||
}
|
||||
|
||||
if (speed.HasValue)
|
||||
{
|
||||
existingState.Speed = speed.Value;
|
||||
}
|
||||
|
||||
state = CloneState(existingState);
|
||||
return true;
|
||||
}
|
||||
|
||||
var resolvedSpeed = speed ?? configuration.MoveSpeed;
|
||||
var createdState = new ServerAuthoritativeMovementState(
|
||||
normalizedSender,
|
||||
playerId,
|
||||
configuration.DefaultHp);
|
||||
configuration.DefaultHp,
|
||||
resolvedSpeed);
|
||||
statesByPeer.Add(key, createdState);
|
||||
state = CloneState(createdState);
|
||||
return true;
|
||||
|
|
@ -343,7 +352,8 @@ namespace Network.NetworkHost
|
|||
Rotation = state.Rotation,
|
||||
IsDead = state.IsDead,
|
||||
InputX = state.InputX,
|
||||
InputY = state.InputY
|
||||
InputY = state.InputY,
|
||||
Speed = state.Speed
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -396,11 +406,11 @@ namespace Network.NetworkHost
|
|||
}
|
||||
|
||||
var rotationRadians = state.Rotation * (MathF.PI / 180f);
|
||||
var forwardX = MathF.Cos(rotationRadians);
|
||||
var forwardZ = MathF.Sin(rotationRadians);
|
||||
state.VelocityX = forwardX * (throttleInput * configuration.MoveSpeed);
|
||||
var forwardX = MathF.Sin(rotationRadians);
|
||||
var forwardZ = MathF.Cos(rotationRadians);
|
||||
state.VelocityX = forwardX * (throttleInput * state.Speed);
|
||||
state.VelocityY = 0f;
|
||||
state.VelocityZ = forwardZ * (throttleInput * configuration.MoveSpeed);
|
||||
state.VelocityZ = forwardZ * (throttleInput * state.Speed);
|
||||
|
||||
var candidatePositionX = state.PositionX + (state.VelocityX * deltaSeconds);
|
||||
var candidatePositionY = state.PositionY + (state.VelocityY * deltaSeconds);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ namespace Network.NetworkHost
|
|||
{
|
||||
public sealed class ServerAuthoritativeMovementState
|
||||
{
|
||||
public ServerAuthoritativeMovementState(IPEndPoint remoteEndPoint, string playerId, int hp)
|
||||
public ServerAuthoritativeMovementState(IPEndPoint remoteEndPoint, string playerId, int hp, float speed = 5f)
|
||||
{
|
||||
RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
|
||||
Hp = hp;
|
||||
IsDead = hp <= 0;
|
||||
Speed = speed;
|
||||
}
|
||||
|
||||
public IPEndPoint RemoteEndPoint { get; }
|
||||
|
|
@ -44,5 +45,7 @@ namespace Network.NetworkHost
|
|||
public float InputX { get; internal set; }
|
||||
|
||||
public float InputY { get; internal set; }
|
||||
|
||||
public float Speed { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ namespace Network.NetworkHost
|
|||
|
||||
public MultiSessionManager SessionCoordinator { get; }
|
||||
|
||||
public float AuthoritativeMoveSpeed => authoritativeMovementCoordinator.MoveSpeed;
|
||||
|
||||
public TimeSpan AuthoritativeMovementCadence => authoritativeMovementCoordinator.SimulationInterval;
|
||||
|
||||
public IReadOnlyList<ManagedNetworkSession> ManagedSessions => SessionCoordinator.Sessions;
|
||||
|
|
@ -268,14 +270,21 @@ namespace Network.NetworkHost
|
|||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
|
||||
BootstrapAuthoritativeMovementState(remoteEndPoint);
|
||||
BootstrapAuthoritativeMovementState(remoteEndPoint, null);
|
||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||
}
|
||||
|
||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId)
|
||||
{
|
||||
NotifyLoginSucceeded(remoteEndPoint, playerId, null);
|
||||
}
|
||||
|
||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId, float? speed)
|
||||
{
|
||||
RememberPlayerId(remoteEndPoint, playerId);
|
||||
NotifyLoginSucceeded(remoteEndPoint);
|
||||
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
|
||||
BootstrapAuthoritativeMovementState(remoteEndPoint, speed);
|
||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||
}
|
||||
|
||||
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
|
||||
|
|
@ -363,14 +372,14 @@ namespace Network.NetworkHost
|
|||
}
|
||||
}
|
||||
|
||||
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
|
||||
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint, float? speed)
|
||||
{
|
||||
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _);
|
||||
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, speed, out _);
|
||||
}
|
||||
|
||||
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Network.Defines;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public class MainUI : MonoBehaviour
|
||||
{
|
||||
|
|
@ -14,6 +18,10 @@ public class MainUI : MonoBehaviour
|
|||
[SerializeField] private Text _correctionText;
|
||||
[SerializeField] private Text _acknowledgedTickText;
|
||||
|
||||
[Header("测试按钮")] [SerializeField] private Button _testOneFrameButton;
|
||||
[SerializeField] private Button _testFiveFramesButton;
|
||||
[SerializeField] private Button _testIntermittentButton;
|
||||
|
||||
public UnityAction<Vector3> OnServerPosChanged;
|
||||
public UnityAction<Vector3> OnClientPosChanged;
|
||||
public UnityAction<long> OnServerTickChanged;
|
||||
|
|
@ -22,6 +30,10 @@ public class MainUI : MonoBehaviour
|
|||
public UnityAction<Vector3, Vector3, float, float> OnCorrectionMagnitudeChanged;
|
||||
public UnityAction<long> OnAcknowledgedMoveTickChanged;
|
||||
|
||||
private const float MoveSpeed = 4f;
|
||||
private const float TurnSpeed = 180f;
|
||||
private const float DeltaTime = 0.05f; // 50ms 模拟步长
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
|
|
@ -36,6 +48,10 @@ public class MainUI : MonoBehaviour
|
|||
OnStartTickOffsetChanged += UpdateStartTickOffsetText;
|
||||
OnCorrectionMagnitudeChanged += UpdateCorrectionText;
|
||||
OnAcknowledgedMoveTickChanged += UpdateAcknowledgedTickText;
|
||||
|
||||
_testOneFrameButton?.onClick.AddListener(OnTestOneFrameClicked);
|
||||
_testFiveFramesButton?.onClick.AddListener(OnTestFiveFramesClicked);
|
||||
_testIntermittentButton?.onClick.AddListener(OnTestIntermittentClicked);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
|
|
@ -47,8 +63,128 @@ public class MainUI : MonoBehaviour
|
|||
OnStartTickOffsetChanged -= UpdateStartTickOffsetText;
|
||||
OnCorrectionMagnitudeChanged -= UpdateCorrectionText;
|
||||
OnAcknowledgedMoveTickChanged -= UpdateAcknowledgedTickText;
|
||||
|
||||
_testOneFrameButton?.onClick.RemoveListener(OnTestOneFrameClicked);
|
||||
_testFiveFramesButton?.onClick.RemoveListener(OnTestFiveFramesClicked);
|
||||
_testIntermittentButton?.onClick.RemoveListener(OnTestIntermittentClicked);
|
||||
}
|
||||
|
||||
// ========== 测试按钮事件 ==========
|
||||
|
||||
private void OnTestOneFrameClicked()
|
||||
{
|
||||
StartCoroutine(RunMovementTestCoroutine(
|
||||
playerId: MasterManager.Instance?.LocalPlayerId ?? "player",
|
||||
inputSequence: new[] { (0f, 1f) }, // 1帧:前进
|
||||
expectedTotalMovement: MoveSpeed * DeltaTime // 0.2
|
||||
));
|
||||
}
|
||||
|
||||
private void OnTestFiveFramesClicked()
|
||||
{
|
||||
StartCoroutine(RunMovementTestCoroutine(
|
||||
playerId: MasterManager.Instance?.LocalPlayerId ?? "player",
|
||||
inputSequence: new[]
|
||||
{
|
||||
(0f, 1f),
|
||||
(0f, 1f),
|
||||
(0f, 1f),
|
||||
(0f, 1f),
|
||||
(0f, 1f)
|
||||
}, // 5帧:连续前进
|
||||
expectedTotalMovement: MoveSpeed * DeltaTime * 5 // 1.0
|
||||
));
|
||||
}
|
||||
|
||||
private void OnTestIntermittentClicked()
|
||||
{
|
||||
StartCoroutine(RunMovementTestCoroutine(
|
||||
playerId: MasterManager.Instance?.LocalPlayerId ?? "player",
|
||||
inputSequence: new[]
|
||||
{
|
||||
(0f, 1f), // 帧1:前进
|
||||
(0f, 1f), // 帧2:前进
|
||||
(0f, 0f), // 帧3:停止
|
||||
(0f, 1f), // 帧4:前进
|
||||
(0f, 1f), // 帧5:前进
|
||||
},
|
||||
expectedTotalMovement: MoveSpeed * DeltaTime * 4 // 0.8(只有4帧在移动)
|
||||
));
|
||||
}
|
||||
|
||||
private IEnumerator RunMovementTestCoroutine(
|
||||
string playerId,
|
||||
(float turn, float throttle)[] inputSequence,
|
||||
float expectedTotalMovement)
|
||||
{
|
||||
if (NetworkManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("[Test] NetworkManager.Instance is null");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var player = MasterManager.Instance?.GetCurrentPlayer();
|
||||
if (player == null)
|
||||
{
|
||||
Debug.LogError("[Test] Current player is null, make sure you're logged in");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var movementComponent = player.GetComponent<MovementComponent>();
|
||||
if (movementComponent == null)
|
||||
{
|
||||
Debug.LogError("[Test] MovementComponent not found on player");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var inputComponent = player.GetComponent<InputComponent>();
|
||||
if (inputComponent == null)
|
||||
{
|
||||
Debug.LogError("[Test] InputComponent not found on player");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 关闭服务器校正,只打印日志不应用位置
|
||||
//movementComponent.SetApplyServerCorrection(false);
|
||||
|
||||
// 保存原始输入源并替换为模拟输入源
|
||||
var originalInputSource = inputComponent.GetInputSource();
|
||||
var simulatedInputSource = new SimulatedInputSource(inputSequence);
|
||||
inputComponent.SetInputSource(simulatedInputSource);
|
||||
//inputComponent.ResetTick(1); // 从 tick 1 开始
|
||||
|
||||
// ========== 运行帧模拟 ==========
|
||||
// 每个输入需要两帧:一帧 Update() 发送 MoveInput,一帧 Advance() 推进
|
||||
for (int i = 0; i < inputSequence.Length; i++)
|
||||
{
|
||||
var (turnInput, throttleInput) = inputSequence[i];
|
||||
|
||||
// yield return null 会触发一帧 Update(),SimulatedInputSource 提供当前输入
|
||||
yield return null;
|
||||
|
||||
Debug.Log($"[Test Tick {i + 1}] Turn={turnInput}, Throttle={throttleInput}");
|
||||
|
||||
// 发送后推进到下一个输入
|
||||
simulatedInputSource.Advance();
|
||||
}
|
||||
|
||||
// 等待一段时间让服务器返回状态
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
|
||||
// 恢复原始输入源
|
||||
inputComponent.SetInputSource(originalInputSource);
|
||||
|
||||
// 恢复服务器校正
|
||||
//movementComponent.SetApplyServerCorrection(true);
|
||||
|
||||
Debug.Log($"========================================");
|
||||
Debug.Log($"[Test: {playerId}]");
|
||||
Debug.Log($"Expected Z Movement: {expectedTotalMovement:F4}");
|
||||
Debug.Log($"Client Position: {player.transform.position}");
|
||||
Debug.Log($"========================================");
|
||||
}
|
||||
|
||||
|
||||
private void UpdateServerPositionText(Vector3 pos)
|
||||
{
|
||||
_serverPositionText.text = "服务端位置:" + pos.ToString();
|
||||
|
|
@ -74,7 +210,8 @@ public class MainUI : MonoBehaviour
|
|||
_clientTickText.text = "客户端Tick:" + tick;
|
||||
}
|
||||
|
||||
private void UpdateCorrectionText(Vector3 predictedPos, Vector3 authoritativePos, float positionError, float rotationError)
|
||||
private void UpdateCorrectionText(Vector3 predictedPos, Vector3 authoritativePos, float positionError,
|
||||
float rotationError)
|
||||
{
|
||||
_correctionText.text = $"校正:pos差={positionError:F4} rot差={rotationError:F2}°";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(serverRuntime.AuthoritativeMovementCadence, Is.EqualTo(TimeSpan.FromMilliseconds(50)));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localServerState), Is.True);
|
||||
Assert.That(localServerState.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(localServerState.PositionX, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(localServerState.PositionZ, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(RemotePeer, out var remoteCombatState), Is.True);
|
||||
Assert.That(remoteCombatState.PlayerId, Is.EqualTo("player-b"));
|
||||
Assert.That(remoteCombatState.Hp, Is.EqualTo(70));
|
||||
|
|
@ -107,7 +107,7 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(clientHarness.TryGetState("player-a", out var localClientState), Is.True);
|
||||
Assert.That(localClientState.Tick, Is.EqualTo(1));
|
||||
Assert.That(localClientState.AcknowledgedMoveTick, Is.EqualTo(1));
|
||||
Assert.That(localClientState.Position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(localClientState.Position.z, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True);
|
||||
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
|
||||
Assert.That(clientHarness.TryGetCombatPresentation("player-b", out var remoteCombatPresentation), Is.True);
|
||||
|
|
@ -228,7 +228,6 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localMovementState), Is.True);
|
||||
Assert.That(localMovementState.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(localMovementState.LastAcceptedMoveTick, Is.EqualTo(0));
|
||||
Assert.That(localMovementState.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(localMovementState.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(ClientPeer, out var localCombatState), Is.True);
|
||||
Assert.That(localCombatState.LastAcceptedShootTick, Is.EqualTo(3));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,277 @@
|
|||
using System;
|
||||
using Network.Defines;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Tests.EditMode.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证客户端 ApplyTankMovement 和服务端 IntegrateState
|
||||
/// 在相同输入下产生相同的移动结果。
|
||||
///
|
||||
/// 使用纯函数对比,不依赖任何状态对象。
|
||||
/// </summary>
|
||||
public class MovementAlgorithmConsistencyTests
|
||||
{
|
||||
private const float MoveSpeed = 4f;
|
||||
private const float TurnSpeed = 180f;
|
||||
private const float DeltaTime = 0.05f; // 50ms
|
||||
|
||||
[Test]
|
||||
public void ServerForward_ZeroRotation_MovesInPositiveZ()
|
||||
{
|
||||
// Arrange: 服务端 rotation=0,throttle=1
|
||||
float rotation = 0f;
|
||||
float throttleInput = 1f;
|
||||
|
||||
// Act: 服务端 forward 计算(新公式)
|
||||
var rotationRadians = rotation * (MathF.PI / 180f);
|
||||
var forwardX = MathF.Sin(rotationRadians); // 新公式
|
||||
var forwardZ = MathF.Cos(rotationRadians); // 新公式
|
||||
var velocityX = forwardX * (throttleInput * MoveSpeed);
|
||||
var velocityZ = forwardZ * (throttleInput * MoveSpeed);
|
||||
|
||||
// Assert: rotation=0° → forward=(0,0,1) = +Z
|
||||
Assert.That(forwardX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(forwardZ, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(velocityX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(velocityZ, Is.EqualTo(4f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ServerForward_90DegreeRotation_MovesInPositiveX()
|
||||
{
|
||||
// Arrange: 服务端 rotation=90°,throttle=1
|
||||
float rotation = 90f;
|
||||
float throttleInput = 1f;
|
||||
|
||||
// Act: 服务端 forward 计算(新公式)
|
||||
var rotationRadians = rotation * (MathF.PI / 180f);
|
||||
var forwardX = MathF.Sin(rotationRadians); // 新公式
|
||||
var forwardZ = MathF.Cos(rotationRadians); // 新公式
|
||||
var velocityX = forwardX * (throttleInput * MoveSpeed);
|
||||
var velocityZ = forwardZ * (throttleInput * MoveSpeed);
|
||||
|
||||
// Assert: rotation=90° → forward=(1,0,0) = +X
|
||||
Assert.That(forwardX, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(forwardZ, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(velocityX, Is.EqualTo(4f).Within(0.0001f));
|
||||
Assert.That(velocityZ, Is.EqualTo(0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientForward_UnityYawZero_MovesInPositiveZ()
|
||||
{
|
||||
// Arrange: Unity Yaw = 0°,客户端转换后 heading = 90°
|
||||
float unityYaw = 0f;
|
||||
float throttleInput = 1f;
|
||||
|
||||
// Act: 客户端 heading 计算
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(unityYaw));
|
||||
|
||||
// 客户端 ResolveHeadingForward: forward = (cos, 0, sin)
|
||||
var rotationRadians = heading * Mathf.Deg2Rad;
|
||||
var forwardX = Mathf.Cos(rotationRadians);
|
||||
var forwardZ = Mathf.Sin(rotationRadians);
|
||||
var velocityX = forwardX * throttleInput * MoveSpeed;
|
||||
var velocityZ = forwardZ * throttleInput * MoveSpeed;
|
||||
|
||||
// Assert: Unity Yaw=0 → heading=90° → forward=(cos(90°), sin(90°))=(0,1) = +Z
|
||||
Assert.That(heading, Is.EqualTo(90f).Within(0.0001f));
|
||||
Assert.That(forwardX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(forwardZ, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(velocityX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(velocityZ, Is.EqualTo(4f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientServer_IdenticalInputs_ProduceIdenticalOutput()
|
||||
{
|
||||
// 这个测试验证:相同的输入在客户端和服务端产生相同的最终位置
|
||||
//
|
||||
// 场景:初始位置 (0,0,0),初始 heading=90°(Unity Yaw=0),
|
||||
// 向前移动 4 个 50ms 步长
|
||||
|
||||
// ===== 共享参数 =====
|
||||
float moveSpeed = MoveSpeed;
|
||||
float turnSpeed = TurnSpeed;
|
||||
float dt = DeltaTime;
|
||||
|
||||
// ===== 服务端计算 =====
|
||||
float serverPosX = 0f, serverPosZ = 0f;
|
||||
float serverRotation = 0f; // 服务端 rotation 直接是 heading
|
||||
float serverThrottle = 1f;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
// 速度计算(服务端新 forward 公式:forwardX = sin, forwardZ = cos)
|
||||
var rotRad = serverRotation * (MathF.PI / 180f);
|
||||
var fwdX = MathF.Sin(rotRad);
|
||||
var fwdZ = MathF.Cos(rotRad);
|
||||
var velX = fwdX * (serverThrottle * moveSpeed);
|
||||
var velZ = fwdZ * (serverThrottle * moveSpeed);
|
||||
|
||||
// 位置积分
|
||||
serverPosX += velX * dt;
|
||||
serverPosZ += velZ * dt;
|
||||
}
|
||||
|
||||
// ===== 客户端计算 =====
|
||||
float clientPosX = 0f, clientPosZ = 0f;
|
||||
float clientUnityYaw = 0f; // Unity Yaw = 0 → heading = 90°
|
||||
float clientThrottle = 1f;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
// 旋转(客户端需要转换)
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw) + (0f * turnSpeed * dt));
|
||||
clientUnityYaw = HeadingToUnityYaw(heading);
|
||||
|
||||
// 客户端 ResolveHeadingForward: forward = (cos, 0, sin)
|
||||
var headingRad = heading * Mathf.Deg2Rad;
|
||||
var fwdX = Mathf.Cos(headingRad);
|
||||
var fwdZ = Mathf.Sin(headingRad);
|
||||
var velX = fwdX * (clientThrottle * moveSpeed);
|
||||
var velZ = fwdZ * (clientThrottle * moveSpeed);
|
||||
|
||||
// 位置积分
|
||||
clientPosX += velX * dt;
|
||||
clientPosZ += velZ * dt;
|
||||
}
|
||||
|
||||
// ===== 对比 =====
|
||||
// 服务端期望:rotation=0° → forward=(0,0,1) → velocity=(0,0,4) → 每步 0.2
|
||||
// 4步后:pos = (0, 0, 0.8)
|
||||
Assert.That(serverPosX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(serverPosZ, Is.EqualTo(0.8f).Within(0.0001f));
|
||||
|
||||
// 客户端应与服务端一致
|
||||
Assert.That(clientPosX, Is.EqualTo(serverPosX).Within(0.0001f),
|
||||
$"Client X ({clientPosX}) should match Server X ({serverPosX})");
|
||||
Assert.That(clientPosZ, Is.EqualTo(serverPosZ).Within(0.0001f),
|
||||
$"Client Z ({clientPosZ}) should match Server Z ({serverPosZ})");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientServer_TurnRight90Degrees_ThenMove_ProduceIdenticalOutput()
|
||||
{
|
||||
// 场景:初始向前走,然后右转90°,再走
|
||||
// 验证转向后的方向一致
|
||||
|
||||
float moveSpeed = MoveSpeed;
|
||||
float turnSpeed = TurnSpeed;
|
||||
float dt = DeltaTime;
|
||||
|
||||
// ===== 服务端计算 =====
|
||||
float serverPosX = 0f, serverPosZ = 0f;
|
||||
float serverRotation = 0f; // heading=0°,向前走
|
||||
float serverThrottle = 1f;
|
||||
float serverTurnInput = 1f; // 右转
|
||||
|
||||
// 先走2步
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var rotRad = serverRotation * (MathF.PI / 180f);
|
||||
var fwdX = MathF.Sin(rotRad);
|
||||
var fwdZ = MathF.Cos(rotRad);
|
||||
var velX = fwdX * serverThrottle * moveSpeed;
|
||||
var velZ = fwdZ * serverThrottle * moveSpeed;
|
||||
serverPosX += velX * dt;
|
||||
serverPosZ += velZ * dt;
|
||||
}
|
||||
|
||||
// 右转1步(turnInput=1,转 180*0.05=9°)
|
||||
serverRotation = NormalizeDegreesServer(serverRotation + (serverTurnInput * turnSpeed * dt));
|
||||
|
||||
// 再走2步
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var rotRad = serverRotation * (MathF.PI / 180f);
|
||||
var fwdX = MathF.Sin(rotRad);
|
||||
var fwdZ = MathF.Cos(rotRad);
|
||||
var velX = fwdX * serverThrottle * moveSpeed;
|
||||
var velZ = fwdZ * serverThrottle * moveSpeed;
|
||||
serverPosX += velX * dt;
|
||||
serverPosZ += velZ * dt;
|
||||
}
|
||||
|
||||
// ===== 客户端计算 =====
|
||||
float clientPosX = 0f, clientPosZ = 0f;
|
||||
float clientUnityYaw = 0f; // 初始朝前
|
||||
float clientThrottle = 1f;
|
||||
float clientTurnInput = -1f; // Unity 中右转 = -input.x
|
||||
|
||||
// 先走2步
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw));
|
||||
var headingRad = heading * Mathf.Deg2Rad;
|
||||
var fwdX = Mathf.Cos(headingRad);
|
||||
var fwdZ = Mathf.Sin(headingRad);
|
||||
var velX = fwdX * clientThrottle * moveSpeed;
|
||||
var velZ = fwdZ * clientThrottle * moveSpeed;
|
||||
clientPosX += velX * dt;
|
||||
clientPosZ += velZ * dt;
|
||||
}
|
||||
|
||||
// 右转1步
|
||||
var newHeading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw) + (clientTurnInput * turnSpeed * dt));
|
||||
clientUnityYaw = HeadingToUnityYaw(newHeading);
|
||||
|
||||
// 再走2步
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw));
|
||||
var headingRad = heading * Mathf.Deg2Rad;
|
||||
var fwdX = Mathf.Cos(headingRad);
|
||||
var fwdZ = Mathf.Sin(headingRad);
|
||||
var velX = fwdX * clientThrottle * moveSpeed;
|
||||
var velZ = fwdZ * clientThrottle * moveSpeed;
|
||||
clientPosX += velX * dt;
|
||||
clientPosZ += velZ * dt;
|
||||
}
|
||||
|
||||
// ===== 对比 =====
|
||||
Assert.That(clientPosX, Is.EqualTo(serverPosX).Within(0.0001f),
|
||||
$"Client X ({clientPosX}) should match Server X ({serverPosX})");
|
||||
Assert.That(clientPosZ, Is.EqualTo(serverPosZ).Within(0.0001f),
|
||||
$"Client Z ({clientPosZ}) should match Server Z ({serverPosZ})");
|
||||
}
|
||||
|
||||
// ===== 辅助方法(从 MovementComponent 复制) =====
|
||||
|
||||
private static float UnityYawToHeading(float unityYawDegrees)
|
||||
{
|
||||
return NormalizeDegrees(90f - unityYawDegrees);
|
||||
}
|
||||
|
||||
private static float HeadingToUnityYaw(float headingDegrees)
|
||||
{
|
||||
return NormalizeDegrees(90f - headingDegrees);
|
||||
}
|
||||
|
||||
private static float NormalizeDegrees(float degrees)
|
||||
{
|
||||
var normalized = degrees % 360f;
|
||||
if (normalized < 0f)
|
||||
{
|
||||
normalized += 360f;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static float NormalizeDegreesServer(float degrees)
|
||||
{
|
||||
var normalized = degrees % 360f;
|
||||
if (normalized <= -180f)
|
||||
{
|
||||
normalized += 360f;
|
||||
}
|
||||
else if (normalized > 180f)
|
||||
{
|
||||
normalized -= 360f;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 351f8ed6c1e2cf047bb173a387d41942
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -55,13 +55,13 @@ namespace Tests.EditMode.Network
|
|||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(1));
|
||||
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterFirstStep), Is.True);
|
||||
Assert.That(stateAfterFirstStep.PositionX, Is.EqualTo(0.2f).Within(0.0001f));
|
||||
Assert.That(stateAfterFirstStep.PositionZ, Is.EqualTo(0.2f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(0));
|
||||
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterSecondStep), Is.True);
|
||||
Assert.That(stateAfterSecondStep.PositionX, Is.EqualTo(0.4f).Within(0.0001f));
|
||||
Assert.That(stateAfterSecondStep.PositionZ, Is.EqualTo(0.4f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
|
|
@ -116,12 +116,12 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerB, out var stateB), Is.True);
|
||||
Assert.That(stateA.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(stateA.LastAcceptedMoveTick, Is.EqualTo(10));
|
||||
Assert.That(stateA.PositionX, Is.EqualTo(0.2f).Within(0.0001f));
|
||||
Assert.That(stateA.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(stateA.PositionZ, Is.EqualTo(0.2f).Within(0.0001f));
|
||||
Assert.That(stateA.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(stateB.PlayerId, Is.EqualTo("player-b"));
|
||||
Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3));
|
||||
Assert.That(stateB.PositionX, Is.EqualTo(-0.2f).Within(0.0001f));
|
||||
Assert.That(stateB.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(stateB.PositionZ, Is.EqualTo(-0.2f).Within(0.0001f));
|
||||
Assert.That(stateB.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
|
|
@ -168,9 +168,9 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(firstBroadcast.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(firstBroadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(firstBroadcast.AcknowledgedMoveTick, Is.EqualTo(1));
|
||||
Assert.That(firstBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.X, Is.EqualTo(10f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Position.Z, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.Z, Is.EqualTo(10f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
|
||||
createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||
{
|
||||
|
|
@ -192,9 +192,9 @@ namespace Tests.EditMode.Network
|
|||
var secondBroadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[1]);
|
||||
Assert.That(secondBroadcast.Tick, Is.EqualTo(2));
|
||||
Assert.That(secondBroadcast.AcknowledgedMoveTick, Is.EqualTo(2));
|
||||
Assert.That(secondBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Position.Z, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -281,9 +281,9 @@ namespace Tests.EditMode.Network
|
|||
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
|
||||
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(broadcast.AcknowledgedMoveTick, Is.EqualTo(5));
|
||||
Assert.That(broadcast.Position.X, Is.EqualTo(-0.3f).Within(0.0001f));
|
||||
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Velocity.X, Is.EqualTo(-6f).Within(0.0001f));
|
||||
Assert.That(broadcast.Position.Z, Is.EqualTo(-0.3f).Within(0.0001f));
|
||||
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Velocity.Z, Is.EqualTo(-6f).Within(0.0001f));
|
||||
}
|
||||
|
||||
private static FakeTransport CreateTransport(IDictionary<int, FakeTransport> createdTransports, int port)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6aee45b7c7c7b734c9b6895d254f1cde
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e1bbd442159abc24a8f56fc7bbad78a8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using Network.NetworkHost;
|
||||
using Network.NetworkTransport;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
namespace Tests.PlayMode.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// PlayMode 测试:使用协程控制帧推进,验证客户端移动与服务端 authoritative state 的一致性。
|
||||
/// 使用真实的 ServerRuntimeEntryPoint,直接获取服务端的 authoritative state 进行对比。
|
||||
/// </summary>
|
||||
public class MovementConsistencyPlayModeTests
|
||||
{
|
||||
private static readonly IPEndPoint ClientPeer = new(IPAddress.Loopback, 9701);
|
||||
|
||||
// 测试参数
|
||||
private const float MoveSpeed = 4f;
|
||||
private const float TurnSpeed = 180f;
|
||||
private const float DeltaTime = 0.05f; // 50ms 模拟步长
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator OneFrame_MoveForward_ClientAndServerPositionMatch()
|
||||
{
|
||||
yield return RunMovementTest(
|
||||
playerId: "player-1frame",
|
||||
inputSequence: new[] { (0f, 1f) }, // 1帧:前进
|
||||
expectedTotalMovement: MoveSpeed * DeltaTime // 4 * 0.05 = 0.2
|
||||
);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator FiveFrames_MoveForward_ClientAndServerPositionMatch()
|
||||
{
|
||||
yield return RunMovementTest(
|
||||
playerId: "player-5frames",
|
||||
inputSequence: new[] { (0f, 1f), (0f, 1f), (0f, 1f), (0f, 1f), (0f, 1f) }, // 5帧:连续前进
|
||||
expectedTotalMovement: MoveSpeed * DeltaTime * 5 // 4 * 0.05 * 5 = 1.0
|
||||
);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator IntermittentMovement_StartStopStart_ClientAndServerPositionMatch()
|
||||
{
|
||||
// 场景:前进2帧 -> 停止1帧 -> 前进2帧
|
||||
yield return RunMovementTest(
|
||||
playerId: "player-intermittent",
|
||||
inputSequence: new[]
|
||||
{
|
||||
(0f, 1f), // 帧1:前进
|
||||
(0f, 1f), // 帧2:前进
|
||||
(0f, 0f), // 帧3:停止
|
||||
(0f, 1f), // 帧4:前进
|
||||
(0f, 1f), // 帧5:前进
|
||||
},
|
||||
expectedTotalMovement: MoveSpeed * DeltaTime * 4 // 4 * 0.05 * 4 = 0.8(只有4帧在移动)
|
||||
);
|
||||
}
|
||||
|
||||
private IEnumerator RunMovementTest(
|
||||
string playerId,
|
||||
(float turn, float throttle)[] inputSequence,
|
||||
float expectedTotalMovement)
|
||||
{
|
||||
// ========== 创建服务器 ==========
|
||||
var serverTransports = new Dictionary<int, FakeTestTransport>();
|
||||
var configuration = new ServerRuntimeConfiguration(9700)
|
||||
{
|
||||
SyncPort = 9701,
|
||||
Dispatcher = new MainThreadNetworkDispatcher(),
|
||||
TransportFactory = port =>
|
||||
{
|
||||
var transport = new FakeTestTransport();
|
||||
serverTransports[port] = transport;
|
||||
return transport;
|
||||
},
|
||||
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
|
||||
{
|
||||
MoveSpeed = MoveSpeed,
|
||||
SimulationInterval = TimeSpan.FromMilliseconds(50),
|
||||
BroadcastInterval = TimeSpan.FromMilliseconds(50)
|
||||
}
|
||||
};
|
||||
|
||||
var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||
|
||||
// ========== 登录 ==========
|
||||
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
|
||||
serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, playerId, MoveSpeed);
|
||||
|
||||
yield return null; // 让服务器初始化
|
||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Debug.Log($"[Setup] Login complete. BroadcastMessages on sync transport: {serverTransports[9701].BroadcastMessages.Count}");
|
||||
|
||||
// ========== 客户端本地计算状态 ==========
|
||||
float clientRotation = 0f; // Unity Yaw = 0 -> heading = 90
|
||||
Vector3 clientPosition = Vector3.zero;
|
||||
|
||||
// ========== 运行帧模拟 ==========
|
||||
int tick = 1;
|
||||
List<Vector3> serverPositions = new List<Vector3>();
|
||||
List<Vector3> clientPositions = new List<Vector3>();
|
||||
|
||||
foreach (var (turnInput, throttleInput) in inputSequence)
|
||||
{
|
||||
// --- 构造 MoveInput ---
|
||||
var moveInput = new MoveInput
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Tick = tick,
|
||||
TurnInput = turnInput,
|
||||
ThrottleInput = throttleInput
|
||||
};
|
||||
|
||||
// --- 发送到服务器 ---
|
||||
var envelope = new Envelope
|
||||
{
|
||||
Type = (int)MessageType.MoveInput,
|
||||
Payload = ByteString.CopyFrom(moveInput.ToByteArray())
|
||||
};
|
||||
Debug.Log($"[Tick {tick}] EmitReceive MoveInput to sync transport (turn={turnInput}, throttle={throttleInput}), BroadcastMessages before: {serverTransports[9701].BroadcastMessages.Count}");
|
||||
serverTransports[9701].EmitReceive(envelope.ToByteArray(), ClientPeer);
|
||||
|
||||
// --- 服务器处理 ---
|
||||
Debug.Log($"[Tick {tick}] Before DrainPendingMessagesAsync, BroadcastMessages: {serverTransports[9701].BroadcastMessages.Count}");
|
||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
Debug.Log($"[Tick {tick}] After DrainPendingMessagesAsync + UpdateAuthoritativeMovement, BroadcastMessages: {serverTransports[9701].BroadcastMessages.Count}");
|
||||
|
||||
// --- 获取服务端 authoritative state(不需要等待广播)---
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var serverState), Is.True);
|
||||
var serverPos = new Vector3(serverState.PositionX, serverState.PositionY, serverState.PositionZ);
|
||||
serverPositions.Add(serverPos);
|
||||
|
||||
// --- 客户端本地计算(与服务端相同的算法) ---
|
||||
var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
|
||||
var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f);
|
||||
|
||||
// 旋转(客户端 Tank Control)
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(clientRotation) + (clampedTurnInput * TurnSpeed * DeltaTime));
|
||||
clientRotation = HeadingToUnityYaw(heading);
|
||||
|
||||
// 速度(客户端 ResolveHeadingForward: forward = (cos, 0, sin))
|
||||
var headingRad = heading * Mathf.Deg2Rad;
|
||||
var forwardX = Mathf.Cos(headingRad);
|
||||
var forwardZ = Mathf.Sin(headingRad);
|
||||
var velocityX = forwardX * (clampedThrottleInput * MoveSpeed);
|
||||
var velocityZ = forwardZ * (clampedThrottleInput * MoveSpeed);
|
||||
|
||||
clientPosition.x += velocityX * DeltaTime;
|
||||
clientPosition.z += velocityZ * DeltaTime;
|
||||
clientPositions.Add(clientPosition);
|
||||
|
||||
Debug.Log($"[Tick {tick}] Turn={turnInput}, Throttle={throttleInput} | " +
|
||||
$"Client=({clientPosition.x:F3}, {clientPosition.y:F3}, {clientPosition.z:F3}) | " +
|
||||
$"Server=({serverPos.x:F3}, {serverPos.y:F3}, {serverPos.z:F3})");
|
||||
|
||||
tick++;
|
||||
yield return null; // 推进一帧
|
||||
}
|
||||
|
||||
// ========== 验证 ==========
|
||||
var finalClientPos = clientPositions[^1];
|
||||
var finalServerPos = serverPositions[^1];
|
||||
|
||||
Debug.Log($"========================================");
|
||||
Debug.Log($"[Test: {playerId}]");
|
||||
Debug.Log($"Expected Z Movement: {expectedTotalMovement}");
|
||||
Debug.Log($"Client Final Z: {finalClientPos.z:F4}");
|
||||
Debug.Log($"Server Final Z: {finalServerPos.z:F4}");
|
||||
Debug.Log($"Server Final Full: ({finalServerPos.x:F4}, {finalServerPos.y:F4}, {finalServerPos.z:F4})");
|
||||
Debug.Log($"========================================");
|
||||
|
||||
float clientMovement = finalClientPos.z;
|
||||
float serverMovement = finalServerPos.z;
|
||||
|
||||
Assert.That(Math.Abs(clientMovement - serverMovement), Is.LessThan(0.01f),
|
||||
$"Client Z movement ({clientMovement:F4}) should match Server Z movement ({serverMovement:F4})");
|
||||
|
||||
Assert.That(Math.Abs(clientMovement - expectedTotalMovement), Is.LessThan(0.01f),
|
||||
$"Movement ({clientMovement:F4}) should match expected ({expectedTotalMovement:F4})");
|
||||
|
||||
// ========== 清理 ==========
|
||||
serverRuntime.Stop();
|
||||
}
|
||||
|
||||
private static float NormalizeDegrees(float degrees)
|
||||
{
|
||||
var normalized = degrees % 360f;
|
||||
if (normalized < 0f) normalized += 360f;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static float UnityYawToHeading(float unityYawDegrees)
|
||||
{
|
||||
return NormalizeDegrees(90f - unityYawDegrees);
|
||||
}
|
||||
|
||||
private static float HeadingToUnityYaw(float headingDegrees)
|
||||
{
|
||||
return NormalizeDegrees(90f - headingDegrees);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 测试辅助类 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 用于 PlayMode 测试的 FakeTransport
|
||||
/// </summary>
|
||||
public class FakeTestTransport : ITransport
|
||||
{
|
||||
private readonly List<byte[]> _receivedMessages = new();
|
||||
public List<byte[]> BroadcastMessages { get; } = new();
|
||||
public event Action<byte[], IPEndPoint> OnReceive;
|
||||
|
||||
public void EmitReceive(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
_receivedMessages.Add(data);
|
||||
OnReceive?.Invoke(data, sender);
|
||||
}
|
||||
|
||||
public void Send(byte[] data) { }
|
||||
public void SendTo(byte[] data, IPEndPoint target) { }
|
||||
public void SendToAll(byte[] data)
|
||||
{
|
||||
BroadcastMessages.Add(data);
|
||||
OnReceive?.Invoke(data, new IPEndPoint(IPAddress.Loopback, 0));
|
||||
}
|
||||
|
||||
public Task StartAsync() => Task.CompletedTask;
|
||||
public void Stop() { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c61c8ae5a68e0fb4195a729749a21019
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "Network.PlayMode.Tests",
|
||||
"rootNamespace": "Tests.PlayMode.Network",
|
||||
"references": [
|
||||
"Network.Runtime",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner",
|
||||
"NetworkFramework"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"nunit.framework.dll",
|
||||
"Google.Protobuf.dll"
|
||||
],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 00dbd4533a02ad04cb7d9dada7210593
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue