单帧测试进行中
This commit is contained in:
parent
90f1832397
commit
1e90de11ce
|
|
@ -31,7 +31,6 @@ RectTransform:
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 6308356813253026692}
|
m_Father: {fileID: 6308356813253026692}
|
||||||
m_RootOrder: 0
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 1, y: 1}
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
|
|
@ -107,7 +106,6 @@ RectTransform:
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 6308356813253026692}
|
m_Father: {fileID: 6308356813253026692}
|
||||||
m_RootOrder: 1
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||||
|
|
@ -191,7 +189,6 @@ RectTransform:
|
||||||
- {fileID: 6308356812888301747}
|
- {fileID: 6308356812888301747}
|
||||||
- {fileID: 6308356813057413814}
|
- {fileID: 6308356813057413814}
|
||||||
m_Father: {fileID: 6308356814245253662}
|
m_Father: {fileID: 6308356814245253662}
|
||||||
m_RootOrder: 0
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
m_AnchorMax: {x: 0, y: 0}
|
||||||
|
|
@ -217,6 +214,7 @@ Canvas:
|
||||||
m_SortingBucketNormalizedSize: 0
|
m_SortingBucketNormalizedSize: 0
|
||||||
m_VertexColorAlwaysGammaSpace: 0
|
m_VertexColorAlwaysGammaSpace: 0
|
||||||
m_AdditionalShaderChannelsFlag: 0
|
m_AdditionalShaderChannelsFlag: 0
|
||||||
|
m_UpdateRectTransformForStandalone: 0
|
||||||
m_SortingLayerID: 0
|
m_SortingLayerID: 0
|
||||||
m_SortingOrder: 0
|
m_SortingOrder: 0
|
||||||
m_TargetDisplay: 0
|
m_TargetDisplay: 0
|
||||||
|
|
@ -299,13 +297,13 @@ Transform:
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 6308356813655391137}
|
m_GameObject: {fileID: 6308356813655391137}
|
||||||
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0.079317145, y: -0, z: -0, w: 0.9968494}
|
m_LocalRotation: {x: 0.079317145, y: -0, z: -0, w: 0.9968494}
|
||||||
m_LocalPosition: {x: -0.26855803, y: 1.55, z: -5.62}
|
m_LocalPosition: {x: -0.26855803, y: 1.55, z: -5.62}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 6308356814245253662}
|
m_Father: {fileID: 6308356814245253662}
|
||||||
m_RootOrder: 1
|
|
||||||
m_LocalEulerAnglesHint: {x: 9.099, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 9.099, y: 0, z: 0}
|
||||||
--- !u!20 &6308356813655391139
|
--- !u!20 &6308356813655391139
|
||||||
Camera:
|
Camera:
|
||||||
|
|
@ -321,9 +319,17 @@ Camera:
|
||||||
m_projectionMatrixMode: 1
|
m_projectionMatrixMode: 1
|
||||||
m_GateFitMode: 2
|
m_GateFitMode: 2
|
||||||
m_FOVAxisMode: 0
|
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_SensorSize: {x: 36, y: 24}
|
||||||
m_LensShift: {x: 0, y: 0}
|
m_LensShift: {x: 0, y: 0}
|
||||||
m_FocalLength: 50
|
|
||||||
m_NormalizedViewPortRect:
|
m_NormalizedViewPortRect:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
x: 0
|
x: 0
|
||||||
|
|
@ -371,6 +377,7 @@ GameObject:
|
||||||
- component: {fileID: 6308356814245253632}
|
- component: {fileID: 6308356814245253632}
|
||||||
- component: {fileID: 6308356814245253634}
|
- component: {fileID: 6308356814245253634}
|
||||||
- component: {fileID: 5069958635149219085}
|
- component: {fileID: 5069958635149219085}
|
||||||
|
- component: {fileID: 8938569698484985372}
|
||||||
- component: {fileID: -1362768914916555191}
|
- component: {fileID: -1362768914916555191}
|
||||||
- component: {fileID: -8136683798838004576}
|
- component: {fileID: -8136683798838004576}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
|
|
@ -387,6 +394,7 @@ Transform:
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 6308356814245253661}
|
m_GameObject: {fileID: 6308356814245253661}
|
||||||
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
|
@ -395,7 +403,6 @@ Transform:
|
||||||
- {fileID: 6308356813253026692}
|
- {fileID: 6308356813253026692}
|
||||||
- {fileID: 6308356813655391140}
|
- {fileID: 6308356813655391140}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_RootOrder: 0
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!33 &6308356814245253633
|
--- !u!33 &6308356814245253633
|
||||||
MeshFilter:
|
MeshFilter:
|
||||||
|
|
@ -482,9 +489,26 @@ MonoBehaviour:
|
||||||
m_Script: {fileID: 11500000, guid: db8117151f564304bae153aa55c0a960, type: 3}
|
m_Script: {fileID: 11500000, guid: db8117151f564304bae153aa55c0a960, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_sendInterval: 0.05
|
_speed: 2
|
||||||
_rigid: {fileID: -1362768914916555191}
|
_rigid: {fileID: -1362768914916555191}
|
||||||
|
_inputComponent: {fileID: 8938569698484985372}
|
||||||
|
_applyServerCorrection: 0
|
||||||
_lerpRate: 0.1
|
_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
|
--- !u!54 &-1362768914916555191
|
||||||
Rigidbody:
|
Rigidbody:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -492,10 +516,21 @@ Rigidbody:
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 6308356814245253661}
|
m_GameObject: {fileID: 6308356814245253661}
|
||||||
serializedVersion: 2
|
serializedVersion: 4
|
||||||
m_Mass: 2
|
m_Mass: 2
|
||||||
m_Drag: 5
|
m_Drag: 5
|
||||||
m_AngularDrag: 0
|
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_UseGravity: 1
|
||||||
m_IsKinematic: 1
|
m_IsKinematic: 1
|
||||||
m_Interpolate: 1
|
m_Interpolate: 1
|
||||||
|
|
@ -509,8 +544,16 @@ BoxCollider:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 6308356814245253661}
|
m_GameObject: {fileID: 6308356814245253661}
|
||||||
m_Material: {fileID: 0}
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
m_IsTrigger: 0
|
m_IsTrigger: 0
|
||||||
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 2
|
serializedVersion: 3
|
||||||
m_Size: {x: 1, y: 1, z: 1}
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
|
|
||||||
|
|
@ -2394,6 +2394,7 @@ RectTransform:
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 1979220485}
|
- {fileID: 1979220485}
|
||||||
- {fileID: 1013475929}
|
- {fileID: 1013475929}
|
||||||
|
- {fileID: 1638923373}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
|
|
@ -2420,6 +2421,9 @@ MonoBehaviour:
|
||||||
_clientTickText: {fileID: 652355035}
|
_clientTickText: {fileID: 652355035}
|
||||||
_correctionText: {fileID: 1413607043}
|
_correctionText: {fileID: 1413607043}
|
||||||
_acknowledgedTickText: {fileID: 532753893}
|
_acknowledgedTickText: {fileID: 532753893}
|
||||||
|
_testOneFrameButton: {fileID: 1997817542}
|
||||||
|
_testFiveFramesButton: {fileID: 1923417043}
|
||||||
|
_testIntermittentButton: {fileID: 1702927428}
|
||||||
--- !u!1 &805112150
|
--- !u!1 &805112150
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -4508,6 +4512,85 @@ MeshFilter:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1360915757}
|
m_GameObject: {fileID: 1360915757}
|
||||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
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
|
--- !u!1 &1413607041
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -5248,6 +5331,44 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &1643006720
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -5565,6 +5686,139 @@ CanvasRenderer:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1676055458}
|
m_GameObject: {fileID: 1676055458}
|
||||||
m_CullTransparentMesh: 1
|
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
|
--- !u!1 &1817537069
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -5707,6 +5961,139 @@ Transform:
|
||||||
- {fileID: 1993386580}
|
- {fileID: 1993386580}
|
||||||
m_Father: {fileID: 1186082400}
|
m_Father: {fileID: 1186082400}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
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
|
--- !u!1 &1937973361
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -5786,6 +6173,164 @@ CanvasRenderer:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1937973361}
|
m_GameObject: {fileID: 1937973361}
|
||||||
m_CullTransparentMesh: 1
|
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
|
--- !u!1 &1979220484
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -5997,6 +6542,139 @@ MeshFilter:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1993386579}
|
m_GameObject: {fileID: 1993386579}
|
||||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
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
|
--- !u!1 &2021317914
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
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 UnityEngine;
|
||||||
using Vector3 = UnityEngine.Vector3;
|
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
|
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 Player _master;
|
||||||
private const float TurnSpeedDegreesPerSecond = 180f;
|
private const float TurnSpeedDegreesPerSecond = 180f;
|
||||||
|
|
||||||
|
// 测试时设为 false,可接收服务器状态日志但不应用校正
|
||||||
|
[SerializeField] private bool _applyServerCorrection = true;
|
||||||
private const float UnityYawOffsetDegrees = 90f;
|
private const float UnityYawOffsetDegrees = 90f;
|
||||||
|
|
||||||
// Server authoritative movement cadence used for replay substepping.
|
// Server authoritative movement cadence used for replay substepping.
|
||||||
// This matches ServerAuthoritativeMovementConfiguration.SimulationInterval (50ms).
|
// This matches ServerAuthoritativeMovementConfiguration.SimulationInterval (50ms).
|
||||||
private const float kServerSimulationStepSeconds = 0.05f;
|
private const float kServerSimulationStepSeconds = 0.05f;
|
||||||
|
|
||||||
private int _speed = 2;
|
|
||||||
[SerializeField] private Rigidbody _rigid;
|
|
||||||
private float _lastSendTime = 0;
|
|
||||||
private bool _isControlled = false;
|
private bool _isControlled = false;
|
||||||
|
|
||||||
private Vector3 _serverPosition;
|
private Vector3 _serverPosition;
|
||||||
|
|
@ -124,14 +31,12 @@ public class MovementComponent : MonoBehaviour
|
||||||
public long Tick { get; private set; } = 0;
|
public long Tick { get; private set; } = 0;
|
||||||
private long _startTickOffset = 0;
|
private long _startTickOffset = 0;
|
||||||
private long _currentTickOffset = 0;
|
private long _currentTickOffset = 0;
|
||||||
|
private float _simulationAccumulator = 0f;
|
||||||
private readonly ClientPredictionBuffer _predictionBuffer = new ClientPredictionBuffer();
|
private readonly ClientPredictionBuffer _predictionBuffer = new ClientPredictionBuffer();
|
||||||
|
|
||||||
private readonly RemotePlayerSnapshotInterpolator _remoteSnapshotInterpolator = new();
|
private readonly RemotePlayerSnapshotInterpolator _remoteSnapshotInterpolator = new();
|
||||||
[SerializeField] private float _lerpRate = 0.1f;
|
[SerializeField] private float _lerpRate = 0.1f;
|
||||||
private Vector3 _cachedMoveInput;
|
|
||||||
private Vector3 _lastAimDirection = Vector3.forward;
|
private Vector3 _lastAimDirection = Vector3.forward;
|
||||||
private bool _wasMovingLastFrame;
|
|
||||||
private bool _stopMessagePending;
|
|
||||||
|
|
||||||
public void Init(bool isControlled, Player master, ClientMovementBootstrap bootstrap)
|
public void Init(bool isControlled, Player master, ClientMovementBootstrap bootstrap)
|
||||||
{
|
{
|
||||||
|
|
@ -148,49 +53,60 @@ public class MovementComponent : MonoBehaviour
|
||||||
_rigid.isKinematic = !isControlled;
|
_rigid.isKinematic = !isControlled;
|
||||||
_rigid.velocity = Vector3.zero;
|
_rigid.velocity = Vector3.zero;
|
||||||
_rigid.angularVelocity = 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()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (_isControlled)
|
if (_isControlled)
|
||||||
{
|
{
|
||||||
_cachedMoveInput = CaptureMovement();
|
if (_inputComponent != null)
|
||||||
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput);
|
|
||||||
if (hasMovement)
|
|
||||||
{
|
{
|
||||||
_stopMessagePending = false;
|
MainUI.Instance.OnClientTickChanged(_inputComponent.CurrentTick);
|
||||||
}
|
|
||||||
else if (_wasMovingLastFrame)
|
|
||||||
{
|
|
||||||
_stopMessagePending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_wasMovingLastFrame = hasMovement;
|
|
||||||
|
|
||||||
var shootInput = CaptureShootInput();
|
|
||||||
if (shootInput != null)
|
|
||||||
{
|
|
||||||
NetworkManager.Instance.SendShootInput(shootInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Time.time - _lastSendTime > _sendInterval)
|
|
||||||
{
|
|
||||||
if (ClientGameplayInputFlow.TryCreateMoveInput(_master.PlayerId, Tick, _cachedMoveInput, _stopMessagePending, out var moveInput))
|
|
||||||
{
|
|
||||||
NetworkManager.Instance.SendMoveInput(moveInput);
|
|
||||||
_predictionBuffer.Record(moveInput);
|
|
||||||
_stopMessagePending = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastSendTime = Time.time;
|
|
||||||
Tick++;
|
|
||||||
|
|
||||||
MainUI.Instance.OnClientTickChanged(Tick);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
// 订阅 InputComponent 的事件来记录预测输入
|
||||||
|
if (_inputComponent != null)
|
||||||
|
{
|
||||||
|
_inputComponent.OnMoveInputCreated += HandleMoveInputCreated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_inputComponent != null)
|
||||||
|
{
|
||||||
|
_inputComponent.OnMoveInputCreated -= HandleMoveInputCreated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMoveInputCreated(MoveInput moveInput)
|
||||||
|
{
|
||||||
|
// 记录到预测缓冲区,用于后续的服务器状态校正和回放
|
||||||
|
_predictionBuffer.Record(moveInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用:设置是否应用服务器状态校正(默认 true)
|
||||||
|
/// 设为 false 时只打印服务器状态日志,不影响本地位置
|
||||||
|
/// </summary>
|
||||||
|
public void SetApplyServerCorrection(bool apply)
|
||||||
|
{
|
||||||
|
_applyServerCorrection = apply;
|
||||||
|
}
|
||||||
|
|
||||||
private void FixedUpdate()
|
private void FixedUpdate()
|
||||||
{
|
{
|
||||||
if (_isControlled)
|
if (_isControlled)
|
||||||
|
|
@ -206,10 +122,17 @@ public class MovementComponent : MonoBehaviour
|
||||||
_hasServerState = false;
|
_hasServerState = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Simulate(_cachedMoveInput);
|
// 累积时间,按服务端 50ms 步长进行模拟
|
||||||
// Use actual elapsed wall-clock time since last authoritative state,
|
_simulationAccumulator += Time.fixedDeltaTime;
|
||||||
// decoupled from FixedUpdate cadence, to match server's 20Hz cadence.
|
while (_simulationAccumulator >= kServerSimulationStepSeconds)
|
||||||
_predictionBuffer.AccumulateWithElapsedTime(Time.time - _predictionBuffer.LastAuthoritativeStateTime);
|
{
|
||||||
|
// 使用最近发送的 MoveInput(来自 predictionBuffer)而非实时输入,
|
||||||
|
// 确保客户端与服务端的输入时序一致
|
||||||
|
Simulate(GetLatestPredictedInput());
|
||||||
|
_simulationAccumulator -= kServerSimulationStepSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:模拟时间现在在 Simulate() 内部通过 AccumulateLatest 累加
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -238,7 +161,8 @@ public class MovementComponent : MonoBehaviour
|
||||||
predictedRotation,
|
predictedRotation,
|
||||||
snapshot.Position,
|
snapshot.Position,
|
||||||
snapshot.RotationQuaternion,
|
snapshot.RotationQuaternion,
|
||||||
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond, snapDistanceMultiplier: 5f),
|
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond,
|
||||||
|
snapDistanceMultiplier: 5f),
|
||||||
_activeVisualCorrection);
|
_activeVisualCorrection);
|
||||||
|
|
||||||
_activeVisualCorrection = correction.NextState;
|
_activeVisualCorrection = correction.NextState;
|
||||||
|
|
@ -246,7 +170,17 @@ public class MovementComponent : MonoBehaviour
|
||||||
_rigid.rotation = correction.Rotation;
|
_rigid.rotation = correction.Rotation;
|
||||||
_rigid.velocity = correction.UsedHardSnap ? snapshot.Velocity : Vector3.zero;
|
_rigid.velocity = correction.UsedHardSnap ? snapshot.Velocity : Vector3.zero;
|
||||||
_rigid.angularVelocity = 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)
|
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()
|
private Vector3 ResolveAimDirection()
|
||||||
{
|
{
|
||||||
var planarForward = Vector3.ProjectOnPlane(_rigid.transform.forward, Vector3.up);
|
var planarForward = Vector3.ProjectOnPlane(_rigid.transform.forward, Vector3.up);
|
||||||
|
|
@ -285,18 +202,29 @@ public class MovementComponent : MonoBehaviour
|
||||||
return planarForward;
|
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)
|
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 (_isControlled)
|
||||||
{
|
{
|
||||||
if (MainUI.Instance != null)
|
if (MainUI.Instance != null)
|
||||||
{
|
{
|
||||||
MainUI.Instance.OnClientPosChanged(_rigid.position);
|
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,7 +233,32 @@ public class MovementComponent : MonoBehaviour
|
||||||
if (_isControlled)
|
if (_isControlled)
|
||||||
{
|
{
|
||||||
_lastAuthoritativeState = snapshot;
|
_lastAuthoritativeState = snapshot;
|
||||||
_hasServerState = true;
|
|
||||||
|
// 打印服务端状态,用于与客户端计算结果对比
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -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)
|
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
|
||||||
{
|
{
|
||||||
foreach (var replayInput in replayInputs)
|
foreach (var replayInput in replayInputs)
|
||||||
|
|
@ -363,13 +338,19 @@ public class MovementComponent : MonoBehaviour
|
||||||
|
|
||||||
var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
|
var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
|
||||||
var clampedThrottleInput = Mathf.Clamp(throttleInput, -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);
|
_rigid.rotation = Quaternion.Euler(0f, HeadingToUnityYaw(heading), 0f);
|
||||||
|
|
||||||
var forward = ResolveHeadingForward(heading);
|
var forward = ResolveHeadingForward(heading);
|
||||||
var velocity = forward * (clampedThrottleInput * _speed);
|
var velocity = forward * (clampedThrottleInput * _speed);
|
||||||
_rigid.velocity = velocity;
|
_rigid.velocity = velocity;
|
||||||
_rigid.position += velocity * deltaTime;
|
_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)
|
private static Vector3 ResolveHeadingForward(float headingDegrees)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,15 @@ namespace Network.NetworkApplication
|
||||||
|
|
||||||
public IReadOnlyList<PredictedMoveStep> PendingInputs => pendingInputs;
|
public IReadOnlyList<PredictedMoveStep> PendingInputs => pendingInputs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空所有 pending inputs。
|
||||||
|
/// 用于 Reconcile 后清理已重放的输入,避免它们的时间被继续累积。
|
||||||
|
/// </summary>
|
||||||
|
public void ClearPendingInputs()
|
||||||
|
{
|
||||||
|
pendingInputs.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
public void Record(MoveInput input)
|
public void Record(MoveInput input)
|
||||||
{
|
{
|
||||||
if (input == null)
|
if (input == null)
|
||||||
|
|
@ -63,7 +72,8 @@ namespace Network.NetworkApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
var latest = pendingInputs[^1];
|
var latest = pendingInputs[^1];
|
||||||
pendingInputs[^1] = new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + simulatedDurationSeconds);
|
pendingInputs[^1] =
|
||||||
|
new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + simulatedDurationSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -79,10 +89,12 @@ namespace Network.NetworkApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
var latest = pendingInputs[^1];
|
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)
|
if (state == null)
|
||||||
{
|
{
|
||||||
|
|
@ -105,5 +117,20 @@ namespace Network.NetworkApplication
|
||||||
_lastAuthoritativeStateTime = currentTime;
|
_lastAuthoritativeStateTime = currentTime;
|
||||||
return true;
|
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) &&
|
if (!movementCoordinator.TryGetState(acceptedPeer, out attackerState) &&
|
||||||
!movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, out attackerState))
|
!movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, null, out attackerState))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ namespace Network.NetworkHost
|
||||||
|
|
||||||
public TimeSpan SimulationInterval => configuration.SimulationInterval;
|
public TimeSpan SimulationInterval => configuration.SimulationInterval;
|
||||||
|
|
||||||
|
public float MoveSpeed => configuration.MoveSpeed;
|
||||||
|
|
||||||
public IReadOnlyList<ServerAuthoritativeMovementState> States
|
public IReadOnlyList<ServerAuthoritativeMovementState> States
|
||||||
{
|
{
|
||||||
get
|
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)
|
if (remoteEndPoint == null)
|
||||||
{
|
{
|
||||||
|
|
@ -73,14 +75,21 @@ namespace Network.NetworkHost
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (speed.HasValue)
|
||||||
|
{
|
||||||
|
existingState.Speed = speed.Value;
|
||||||
|
}
|
||||||
|
|
||||||
state = CloneState(existingState);
|
state = CloneState(existingState);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resolvedSpeed = speed ?? configuration.MoveSpeed;
|
||||||
var createdState = new ServerAuthoritativeMovementState(
|
var createdState = new ServerAuthoritativeMovementState(
|
||||||
normalizedSender,
|
normalizedSender,
|
||||||
playerId,
|
playerId,
|
||||||
configuration.DefaultHp);
|
configuration.DefaultHp,
|
||||||
|
resolvedSpeed);
|
||||||
statesByPeer.Add(key, createdState);
|
statesByPeer.Add(key, createdState);
|
||||||
state = CloneState(createdState);
|
state = CloneState(createdState);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -343,7 +352,8 @@ namespace Network.NetworkHost
|
||||||
Rotation = state.Rotation,
|
Rotation = state.Rotation,
|
||||||
IsDead = state.IsDead,
|
IsDead = state.IsDead,
|
||||||
InputX = state.InputX,
|
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 rotationRadians = state.Rotation * (MathF.PI / 180f);
|
||||||
var forwardX = MathF.Cos(rotationRadians);
|
var forwardX = MathF.Sin(rotationRadians);
|
||||||
var forwardZ = MathF.Sin(rotationRadians);
|
var forwardZ = MathF.Cos(rotationRadians);
|
||||||
state.VelocityX = forwardX * (throttleInput * configuration.MoveSpeed);
|
state.VelocityX = forwardX * (throttleInput * state.Speed);
|
||||||
state.VelocityY = 0f;
|
state.VelocityY = 0f;
|
||||||
state.VelocityZ = forwardZ * (throttleInput * configuration.MoveSpeed);
|
state.VelocityZ = forwardZ * (throttleInput * state.Speed);
|
||||||
|
|
||||||
var candidatePositionX = state.PositionX + (state.VelocityX * deltaSeconds);
|
var candidatePositionX = state.PositionX + (state.VelocityX * deltaSeconds);
|
||||||
var candidatePositionY = state.PositionY + (state.VelocityY * deltaSeconds);
|
var candidatePositionY = state.PositionY + (state.VelocityY * deltaSeconds);
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ namespace Network.NetworkHost
|
||||||
{
|
{
|
||||||
public sealed class ServerAuthoritativeMovementState
|
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));
|
RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||||
PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
|
PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
|
||||||
Hp = hp;
|
Hp = hp;
|
||||||
IsDead = hp <= 0;
|
IsDead = hp <= 0;
|
||||||
|
Speed = speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPEndPoint RemoteEndPoint { get; }
|
public IPEndPoint RemoteEndPoint { get; }
|
||||||
|
|
@ -44,5 +45,7 @@ namespace Network.NetworkHost
|
||||||
public float InputX { get; internal set; }
|
public float InputX { get; internal set; }
|
||||||
|
|
||||||
public float InputY { 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 MultiSessionManager SessionCoordinator { get; }
|
||||||
|
|
||||||
|
public float AuthoritativeMoveSpeed => authoritativeMovementCoordinator.MoveSpeed;
|
||||||
|
|
||||||
public TimeSpan AuthoritativeMovementCadence => authoritativeMovementCoordinator.SimulationInterval;
|
public TimeSpan AuthoritativeMovementCadence => authoritativeMovementCoordinator.SimulationInterval;
|
||||||
|
|
||||||
public IReadOnlyList<ManagedNetworkSession> ManagedSessions => SessionCoordinator.Sessions;
|
public IReadOnlyList<ManagedNetworkSession> ManagedSessions => SessionCoordinator.Sessions;
|
||||||
|
|
@ -268,14 +270,21 @@ namespace Network.NetworkHost
|
||||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint)
|
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint)
|
||||||
{
|
{
|
||||||
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
|
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
|
||||||
BootstrapAuthoritativeMovementState(remoteEndPoint);
|
BootstrapAuthoritativeMovementState(remoteEndPoint, null);
|
||||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId)
|
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId)
|
||||||
|
{
|
||||||
|
NotifyLoginSucceeded(remoteEndPoint, playerId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId, float? speed)
|
||||||
{
|
{
|
||||||
RememberPlayerId(remoteEndPoint, playerId);
|
RememberPlayerId(remoteEndPoint, playerId);
|
||||||
NotifyLoginSucceeded(remoteEndPoint);
|
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
|
||||||
|
BootstrapAuthoritativeMovementState(remoteEndPoint, speed);
|
||||||
|
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
|
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))
|
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _);
|
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, speed, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
|
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;
|
||||||
using UnityEngine.Events;
|
using UnityEngine.Events;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using Vector3 = UnityEngine.Vector3;
|
||||||
|
|
||||||
public class MainUI : MonoBehaviour
|
public class MainUI : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
|
@ -14,6 +18,10 @@ public class MainUI : MonoBehaviour
|
||||||
[SerializeField] private Text _correctionText;
|
[SerializeField] private Text _correctionText;
|
||||||
[SerializeField] private Text _acknowledgedTickText;
|
[SerializeField] private Text _acknowledgedTickText;
|
||||||
|
|
||||||
|
[Header("测试按钮")] [SerializeField] private Button _testOneFrameButton;
|
||||||
|
[SerializeField] private Button _testFiveFramesButton;
|
||||||
|
[SerializeField] private Button _testIntermittentButton;
|
||||||
|
|
||||||
public UnityAction<Vector3> OnServerPosChanged;
|
public UnityAction<Vector3> OnServerPosChanged;
|
||||||
public UnityAction<Vector3> OnClientPosChanged;
|
public UnityAction<Vector3> OnClientPosChanged;
|
||||||
public UnityAction<long> OnServerTickChanged;
|
public UnityAction<long> OnServerTickChanged;
|
||||||
|
|
@ -22,6 +30,10 @@ public class MainUI : MonoBehaviour
|
||||||
public UnityAction<Vector3, Vector3, float, float> OnCorrectionMagnitudeChanged;
|
public UnityAction<Vector3, Vector3, float, float> OnCorrectionMagnitudeChanged;
|
||||||
public UnityAction<long> OnAcknowledgedMoveTickChanged;
|
public UnityAction<long> OnAcknowledgedMoveTickChanged;
|
||||||
|
|
||||||
|
private const float MoveSpeed = 4f;
|
||||||
|
private const float TurnSpeed = 180f;
|
||||||
|
private const float DeltaTime = 0.05f; // 50ms 模拟步长
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this;
|
||||||
|
|
@ -36,6 +48,10 @@ public class MainUI : MonoBehaviour
|
||||||
OnStartTickOffsetChanged += UpdateStartTickOffsetText;
|
OnStartTickOffsetChanged += UpdateStartTickOffsetText;
|
||||||
OnCorrectionMagnitudeChanged += UpdateCorrectionText;
|
OnCorrectionMagnitudeChanged += UpdateCorrectionText;
|
||||||
OnAcknowledgedMoveTickChanged += UpdateAcknowledgedTickText;
|
OnAcknowledgedMoveTickChanged += UpdateAcknowledgedTickText;
|
||||||
|
|
||||||
|
_testOneFrameButton?.onClick.AddListener(OnTestOneFrameClicked);
|
||||||
|
_testFiveFramesButton?.onClick.AddListener(OnTestFiveFramesClicked);
|
||||||
|
_testIntermittentButton?.onClick.AddListener(OnTestIntermittentClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
|
|
@ -47,8 +63,128 @@ public class MainUI : MonoBehaviour
|
||||||
OnStartTickOffsetChanged -= UpdateStartTickOffsetText;
|
OnStartTickOffsetChanged -= UpdateStartTickOffsetText;
|
||||||
OnCorrectionMagnitudeChanged -= UpdateCorrectionText;
|
OnCorrectionMagnitudeChanged -= UpdateCorrectionText;
|
||||||
OnAcknowledgedMoveTickChanged -= UpdateAcknowledgedTickText;
|
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)
|
private void UpdateServerPositionText(Vector3 pos)
|
||||||
{
|
{
|
||||||
_serverPositionText.text = "服务端位置:" + pos.ToString();
|
_serverPositionText.text = "服务端位置:" + pos.ToString();
|
||||||
|
|
@ -74,7 +210,8 @@ public class MainUI : MonoBehaviour
|
||||||
_clientTickText.text = "客户端Tick:" + tick;
|
_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}°";
|
_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.AuthoritativeMovementCadence, Is.EqualTo(TimeSpan.FromMilliseconds(50)));
|
||||||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localServerState), Is.True);
|
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localServerState), Is.True);
|
||||||
Assert.That(localServerState.PlayerId, Is.EqualTo("player-a"));
|
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(serverRuntime.TryGetAuthoritativeCombatState(RemotePeer, out var remoteCombatState), Is.True);
|
||||||
Assert.That(remoteCombatState.PlayerId, Is.EqualTo("player-b"));
|
Assert.That(remoteCombatState.PlayerId, Is.EqualTo("player-b"));
|
||||||
Assert.That(remoteCombatState.Hp, Is.EqualTo(70));
|
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(clientHarness.TryGetState("player-a", out var localClientState), Is.True);
|
||||||
Assert.That(localClientState.Tick, Is.EqualTo(1));
|
Assert.That(localClientState.Tick, Is.EqualTo(1));
|
||||||
Assert.That(localClientState.AcknowledgedMoveTick, 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(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True);
|
||||||
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
|
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
|
||||||
Assert.That(clientHarness.TryGetCombatPresentation("player-b", out var remoteCombatPresentation), Is.True);
|
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(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localMovementState), Is.True);
|
||||||
Assert.That(localMovementState.PlayerId, Is.EqualTo("player-a"));
|
Assert.That(localMovementState.PlayerId, Is.EqualTo("player-a"));
|
||||||
Assert.That(localMovementState.LastAcceptedMoveTick, Is.EqualTo(0));
|
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(localMovementState.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||||
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(ClientPeer, out var localCombatState), Is.True);
|
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(ClientPeer, out var localCombatState), Is.True);
|
||||||
Assert.That(localCombatState.LastAcceptedShootTick, Is.EqualTo(3));
|
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));
|
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(1));
|
||||||
|
|
||||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterFirstStep), Is.True);
|
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));
|
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(0));
|
||||||
|
|
||||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||||
|
|
||||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterSecondStep), Is.True);
|
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));
|
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(runtime.TryGetAuthoritativeMovementState(PeerB, out var stateB), Is.True);
|
||||||
Assert.That(stateA.PlayerId, Is.EqualTo("player-a"));
|
Assert.That(stateA.PlayerId, Is.EqualTo("player-a"));
|
||||||
Assert.That(stateA.LastAcceptedMoveTick, Is.EqualTo(10));
|
Assert.That(stateA.LastAcceptedMoveTick, Is.EqualTo(10));
|
||||||
Assert.That(stateA.PositionX, Is.EqualTo(0.2f).Within(0.0001f));
|
Assert.That(stateA.PositionZ, Is.EqualTo(0.2f).Within(0.0001f));
|
||||||
Assert.That(stateA.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
Assert.That(stateA.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||||
Assert.That(stateB.PlayerId, Is.EqualTo("player-b"));
|
Assert.That(stateB.PlayerId, Is.EqualTo("player-b"));
|
||||||
Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3));
|
Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3));
|
||||||
Assert.That(stateB.PositionX, Is.EqualTo(-0.2f).Within(0.0001f));
|
Assert.That(stateB.PositionZ, Is.EqualTo(-0.2f).Within(0.0001f));
|
||||||
Assert.That(stateB.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
Assert.That(stateB.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(2));
|
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.PlayerId, Is.EqualTo("player-a"));
|
||||||
Assert.That(firstBroadcast.Tick, Is.EqualTo(1));
|
Assert.That(firstBroadcast.Tick, Is.EqualTo(1));
|
||||||
Assert.That(firstBroadcast.AcknowledgedMoveTick, Is.EqualTo(1));
|
Assert.That(firstBroadcast.AcknowledgedMoveTick, Is.EqualTo(1));
|
||||||
Assert.That(firstBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
Assert.That(firstBroadcast.Position.Z, Is.EqualTo(1f).Within(0.0001f));
|
||||||
Assert.That(firstBroadcast.Velocity.X, Is.EqualTo(10f).Within(0.0001f));
|
Assert.That(firstBroadcast.Velocity.Z, Is.EqualTo(10f).Within(0.0001f));
|
||||||
Assert.That(firstBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
Assert.That(firstBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||||
|
|
||||||
createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||||
{
|
{
|
||||||
|
|
@ -192,9 +192,9 @@ namespace Tests.EditMode.Network
|
||||||
var secondBroadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[1]);
|
var secondBroadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[1]);
|
||||||
Assert.That(secondBroadcast.Tick, Is.EqualTo(2));
|
Assert.That(secondBroadcast.Tick, Is.EqualTo(2));
|
||||||
Assert.That(secondBroadcast.AcknowledgedMoveTick, Is.EqualTo(2));
|
Assert.That(secondBroadcast.AcknowledgedMoveTick, Is.EqualTo(2));
|
||||||
Assert.That(secondBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
Assert.That(secondBroadcast.Position.Z, Is.EqualTo(1f).Within(0.0001f));
|
||||||
Assert.That(secondBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
|
||||||
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).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]
|
[Test]
|
||||||
|
|
@ -281,9 +281,9 @@ namespace Tests.EditMode.Network
|
||||||
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
|
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
|
||||||
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
||||||
Assert.That(broadcast.AcknowledgedMoveTick, Is.EqualTo(5));
|
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(-0.3f).Within(0.0001f));
|
||||||
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
|
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
|
||||||
Assert.That(broadcast.Velocity.X, Is.EqualTo(-6f).Within(0.0001f));
|
Assert.That(broadcast.Velocity.Z, Is.EqualTo(-6f).Within(0.0001f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FakeTransport CreateTransport(IDictionary<int, FakeTransport> createdTransports, int port)
|
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