CHANGED TO MIRROR

This commit is contained in:
DerTyp187
2021-10-25 09:20:01 +02:00
parent bd712107b7
commit e509a919b6
611 changed files with 38291 additions and 1216 deletions

930
Assets/DebugCanvas.prefab Normal file
View File

@@ -0,0 +1,930 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &4636852677861728911
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4636852677861728898}
- component: {fileID: 4636852677861728899}
- component: {fileID: 4636852677861728908}
- component: {fileID: 4636852677861728909}
- component: {fileID: 4636852677861728910}
m_Layer: 5
m_Name: DebugCanvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4636852677861728898
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677861728911}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_Children:
- {fileID: 4636852679938327861}
- {fileID: 4636852678745905426}
- {fileID: 4636852679900821488}
- {fileID: 4636852679675792567}
- {fileID: 4636852679318150298}
- {fileID: 4636852677968575730}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!223 &4636852677861728899
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677861728911}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_AdditionalShaderChannelsFlag: 25
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!114 &4636852677861728908
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677861728911}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!114 &4636852677861728909
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677861728911}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &4636852677861728910
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677861728911}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 836e95000cf92fa4b9f08812101723e7, type: 3}
m_Name:
m_EditorClassIdentifier:
DebugTextGrounded: {fileID: 4636852678745905425}
DebugTextClientServer: {fileID: 4636852679318150297}
Player: {fileID: 0}
GameManager: {fileID: 0}
fpsText: {fileID: 4636852679900821490}
deltaTime: 0
--- !u!1 &4636852677968575731
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4636852677968575730}
- component: {fileID: 4636852677968575728}
- component: {fileID: 4636852677968575729}
m_Layer: 5
m_Name: DebugHelpText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4636852677968575730
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677968575731}
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_Children: []
m_Father: {fileID: 4636852677861728898}
m_RootOrder: 5
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: -0.2000122, y: -5.9759827}
m_SizeDelta: {x: 560.5, y: 40.112}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &4636852677968575728
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677968575731}
m_CullTransparentMesh: 1
--- !u!114 &4636852677968575729
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852677968575731}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, 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_text: '[J] Join | [H] Host'
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4278193919
m_fontColor: {r: 1, g: 0.053233847, b: 0, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 15
m_fontSizeBase: 15
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 1
m_HorizontalAlignment: 2
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &4636852678745905427
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4636852678745905426}
- component: {fileID: 4636852678745905424}
- component: {fileID: 4636852678745905425}
m_Layer: 5
m_Name: DebugTextGrounded
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4636852678745905426
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852678745905427}
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_Children: []
m_Father: {fileID: 4636852677861728898}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -14, y: -36.2}
m_SizeDelta: {x: 560.5, y: 50}
m_Pivot: {x: 1, y: 1}
--- !u!222 &4636852678745905424
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852678745905427}
m_CullTransparentMesh: 1
--- !u!114 &4636852678745905425
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852678745905427}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, 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_text: GroundCheck
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4278451968
m_fontColor: {r: 0, g: 1, b: 0.013357401, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 15
m_fontSizeBase: 15
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 1
m_HorizontalAlignment: 4
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &4636852679318150299
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4636852679318150298}
- component: {fileID: 4636852679318150296}
- component: {fileID: 4636852679318150297}
m_Layer: 5
m_Name: DebugTextClientServer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4636852679318150298
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679318150299}
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_Children: []
m_Father: {fileID: 4636852677861728898}
m_RootOrder: 4
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -14, y: -55.2}
m_SizeDelta: {x: 560.5, y: 40.112}
m_Pivot: {x: 1, y: 1}
--- !u!222 &4636852679318150296
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679318150299}
m_CullTransparentMesh: 1
--- !u!114 &4636852679318150297
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679318150299}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, 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_text: Server/Client
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294934528
m_fontColor: {r: 0, g: 0.50070286, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 15
m_fontSizeBase: 15
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 1
m_HorizontalAlignment: 4
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &4636852679675792560
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4636852679675792567}
- component: {fileID: 4636852679675792565}
- component: {fileID: 4636852679675792566}
m_Layer: 5
m_Name: Dice
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4636852679675792567
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679675792560}
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_Children: []
m_Father: {fileID: 4636852677861728898}
m_RootOrder: 3
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: 200, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4636852679675792565
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679675792560}
m_CullTransparentMesh: 1
--- !u!114 &4636852679675792566
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679675792560}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, 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_text:
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281204480
m_fontColor: {r: 0, g: 1, b: 0.17560244, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 36
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 1
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &4636852679900821491
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4636852679900821488}
- component: {fileID: 4636852679900821489}
- component: {fileID: 4636852679900821490}
m_Layer: 5
m_Name: DebugFPS
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4636852679900821488
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679900821491}
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_Children: []
m_Father: {fileID: 4636852677861728898}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 5.3916016, y: -17.471985}
m_SizeDelta: {x: 88.783295, y: 28.615997}
m_Pivot: {x: 1, y: 1}
--- !u!222 &4636852679900821489
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679900821491}
m_CullTransparentMesh: 1
--- !u!114 &4636852679900821490
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679900821491}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, 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_text: 187 FPS
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4278255394
m_fontColor: {r: 0.1348741, g: 1, b: 0, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 14
m_fontSizeBase: 14
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 1
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 2.1383667, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &4636852679938327862
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4636852679938327861}
- component: {fileID: 4636852679938328011}
- component: {fileID: 4636852679938327860}
m_Layer: 5
m_Name: DebugTitle
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4636852679938327861
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679938327862}
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_Children: []
m_Father: {fileID: 4636852677861728898}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -14, y: 0.000030517578}
m_SizeDelta: {x: 560.5, y: 17.470001}
m_Pivot: {x: 1, y: 1}
--- !u!222 &4636852679938328011
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679938327862}
m_CullTransparentMesh: 1
--- !u!114 &4636852679938327860
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4636852679938327862}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, 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_text: Debug Info
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4278197759
m_fontColor: {r: 1, g: 0.11461386, b: 0, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 15
m_fontSizeBase: 15
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 1
m_HorizontalAlignment: 4
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e4f2c82228df5bc4f8fc674e98c7275b
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Assets/Mirror.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5cf8eb36be0834b3da408c694a41cb88
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1b2f9d254154cd942ba40b06b869b8f3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,178 @@
using System;
using System.Collections;
using UnityEngine;
namespace Mirror.Authenticators
{
[AddComponentMenu("Network/Authenticators/BasicAuthenticator")]
public class BasicAuthenticator : NetworkAuthenticator
{
[Header("Custom Properties")]
// set these in the inspector
public string username;
public string password;
#region Messages
public struct AuthRequestMessage : NetworkMessage
{
// use whatever credentials make sense for your game
// for example, you might want to pass the accessToken if using oauth
public string authUsername;
public string authPassword;
}
public struct AuthResponseMessage : NetworkMessage
{
public byte code;
public string message;
}
#endregion
#region Server
/// <summary>
/// Called on server from StartServer to initialize the Authenticator
/// <para>Server message handlers should be registered in this method.</para>
/// </summary>
public override void OnStartServer()
{
// register a handler for the authentication request we expect from client
NetworkServer.RegisterHandler<AuthRequestMessage>(OnAuthRequestMessage, false);
}
/// <summary>
/// Called on server from StopServer to reset the Authenticator
/// <para>Server message handlers should be registered in this method.</para>
/// </summary>
public override void OnStopServer()
{
// unregister the handler for the authentication request
NetworkServer.UnregisterHandler<AuthRequestMessage>();
}
/// <summary>
/// Called on server from OnServerAuthenticateInternal when a client needs to authenticate
/// </summary>
/// <param name="conn">Connection to client.</param>
public override void OnServerAuthenticate(NetworkConnection conn)
{
// do nothing...wait for AuthRequestMessage from client
}
/// <summary>
/// Called on server when the client's AuthRequestMessage arrives
/// </summary>
/// <param name="conn">Connection to client.</param>
/// <param name="msg">The message payload</param>
public void OnAuthRequestMessage(NetworkConnection conn, AuthRequestMessage msg)
{
// Debug.LogFormat(LogType.Log, "Authentication Request: {0} {1}", msg.authUsername, msg.authPassword);
// check the credentials by calling your web server, database table, playfab api, or any method appropriate.
if (msg.authUsername == username && msg.authPassword == password)
{
// create and send msg to client so it knows to proceed
AuthResponseMessage authResponseMessage = new AuthResponseMessage
{
code = 100,
message = "Success"
};
conn.Send(authResponseMessage);
// Accept the successful authentication
ServerAccept(conn);
}
else
{
// create and send msg to client so it knows to disconnect
AuthResponseMessage authResponseMessage = new AuthResponseMessage
{
code = 200,
message = "Invalid Credentials"
};
conn.Send(authResponseMessage);
// must set NetworkConnection isAuthenticated = false
conn.isAuthenticated = false;
// disconnect the client after 1 second so that response message gets delivered
StartCoroutine(DelayedDisconnect(conn, 1));
}
}
IEnumerator DelayedDisconnect(NetworkConnection conn, float waitTime)
{
yield return new WaitForSeconds(waitTime);
// Reject the unsuccessful authentication
ServerReject(conn);
}
#endregion
#region Client
/// <summary>
/// Called on client from StartClient to initialize the Authenticator
/// <para>Client message handlers should be registered in this method.</para>
/// </summary>
public override void OnStartClient()
{
// register a handler for the authentication response we expect from server
NetworkClient.RegisterHandler<AuthResponseMessage>((Action<AuthResponseMessage>)OnAuthResponseMessage, false);
}
/// <summary>
/// Called on client from StopClient to reset the Authenticator
/// <para>Client message handlers should be unregistered in this method.</para>
/// </summary>
public override void OnStopClient()
{
// unregister the handler for the authentication response
NetworkClient.UnregisterHandler<AuthResponseMessage>();
}
/// <summary>
/// Called on client from OnClientAuthenticateInternal when a client needs to authenticate
/// </summary>
public override void OnClientAuthenticate()
{
AuthRequestMessage authRequestMessage = new AuthRequestMessage
{
authUsername = username,
authPassword = password
};
NetworkClient.connection.Send(authRequestMessage);
}
/// <summary>
/// Called on client when the server's AuthResponseMessage arrives
/// </summary>
/// <param name="msg">The message payload</param>
public void OnAuthResponseMessage(AuthResponseMessage msg)
{
if (msg.code == 100)
{
// Debug.LogFormat(LogType.Log, "Authentication Response: {0}", msg.message);
// Authentication has been accepted
ClientAccept();
}
else
{
Debug.LogError($"Authentication Response: {msg.message}");
// Authentication has been rejected
ClientReject();
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 28496b776660156428f00cf78289c1ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "Mirror.Authenticators",
"references": [
"Mirror"
],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e720aa64e3f58fb4880566a322584340
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,70 @@
using System.Collections;
using UnityEngine;
namespace Mirror.Authenticators
{
/// <summary>
/// An authenticator that disconnects connections if they don't
/// authenticate within a specified time limit.
/// </summary>
[AddComponentMenu("Network/Authenticators/TimeoutAuthenticator")]
public class TimeoutAuthenticator : NetworkAuthenticator
{
public NetworkAuthenticator authenticator;
[Range(0, 600), Tooltip("Timeout to auto-disconnect in seconds. Set to 0 for no timeout.")]
public float timeout = 60;
public void Awake()
{
authenticator.OnServerAuthenticated.AddListener(connection => OnServerAuthenticated.Invoke(connection));
authenticator.OnClientAuthenticated.AddListener(connection => OnClientAuthenticated.Invoke(connection));
}
public override void OnStartServer()
{
authenticator.OnStartServer();
}
public override void OnStopServer()
{
authenticator.OnStopServer();
}
public override void OnStartClient()
{
authenticator.OnStartClient();
}
public override void OnStopClient()
{
authenticator.OnStopClient();
}
public override void OnServerAuthenticate(NetworkConnection conn)
{
authenticator.OnServerAuthenticate(conn);
if (timeout > 0)
StartCoroutine(BeginAuthentication(conn));
}
public override void OnClientAuthenticate()
{
authenticator.OnClientAuthenticate();
if (timeout > 0)
StartCoroutine(BeginAuthentication(NetworkClient.connection));
}
IEnumerator BeginAuthentication(NetworkConnection conn)
{
// Debug.Log($"Authentication countdown started {conn} {timeout}");
yield return new WaitForSecondsRealtime(timeout);
if (!conn.isAuthenticated)
{
// Debug.Log($"Authentication Timeout {conn}");
conn.Disconnect();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 24d8269a07b8e4edfa374753a91c946e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1f8b918bcd89f5c488b06f5574f34760
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "Mirror.CompilerSymbols",
"references": [],
"optionalUnityReferences": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 325984b52e4128546bc7558552f8b1d2
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using UnityEditor;
namespace Mirror
{
static class PreprocessorDefine
{
/// <summary>
/// Add define symbols as soon as Unity gets done compiling.
/// </summary>
[InitializeOnLoadMethod]
public static void AddDefineSymbols()
{
string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
HashSet<string> defines = new HashSet<string>(currentDefines.Split(';'))
{
"MIRROR",
"MIRROR_17_0_OR_NEWER",
"MIRROR_18_0_OR_NEWER",
"MIRROR_24_0_OR_NEWER",
"MIRROR_26_0_OR_NEWER",
"MIRROR_27_0_OR_NEWER",
"MIRROR_28_0_OR_NEWER",
"MIRROR_29_0_OR_NEWER",
"MIRROR_30_0_OR_NEWER",
"MIRROR_30_5_2_OR_NEWER",
"MIRROR_32_1_2_OR_NEWER",
"MIRROR_32_1_4_OR_NEWER",
"MIRROR_35_0_OR_NEWER",
"MIRROR_35_1_OR_NEWER",
"MIRROR_37_0_OR_NEWER",
"MIRROR_38_0_OR_NEWER",
"MIRROR_39_0_OR_NEWER",
"MIRROR_40_0_OR_NEWER",
"MIRROR_41_0_OR_NEWER",
"MIRROR_42_0_OR_NEWER",
"MIRROR_43_0_OR_NEWER",
"MIRROR_44_0_OR_NEWER",
"MIRROR_46_0_OR_NEWER",
"MIRROR_47_0_OR_NEWER",
"MIRROR_53_0_OR_NEWER"
};
// only touch PlayerSettings if we actually modified it.
// otherwise it shows up as changed in git each time.
string newDefines = string.Join(";", defines);
if (newDefines != currentDefines)
{
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines);
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: bfd6cd3bbcb023440832d53f9f1f0041
guid: f1d66fe74ec6f42dd974cba37d25d453
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9bee879fbc8ef4b1a9a9f7088bfbf726
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Mirror.Tests.Common")]
[assembly: InternalsVisibleTo("Mirror.Tests")]
// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline
// for ILPostProcessor tests.
[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")]
[assembly: InternalsVisibleTo("Mirror.Tests.Generated")]
[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")]
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
[assembly: InternalsVisibleTo("Mirror.Editor")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a65b9283f7a724e70b8e17cb277f4c1e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b5dcf9618f5e14a4eb60bff5480284a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
using System;
using System.Net;
using UnityEngine;
using UnityEngine.Events;
namespace Mirror.Discovery
{
[Serializable]
public class ServerFoundUnityEvent : UnityEvent<ServerResponse> {};
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkDiscovery")]
public class NetworkDiscovery : NetworkDiscoveryBase<ServerRequest, ServerResponse>
{
#region Server
public long ServerId { get; private set; }
[Tooltip("Transport to be advertised during discovery")]
public Transport transport;
[Tooltip("Invoked when a server is found")]
public ServerFoundUnityEvent OnServerFound;
public override void Start()
{
ServerId = RandomLong();
// active transport gets initialized in awake
// so make sure we set it here in Start() (after awakes)
// Or just let the user assign it in the inspector
if (transport == null)
transport = Transport.activeTransport;
base.Start();
}
/// <summary>
/// Process the request from a client
/// </summary>
/// <remarks>
/// Override if you wish to provide more information to the clients
/// such as the name of the host player
/// </remarks>
/// <param name="request">Request coming from client</param>
/// <param name="endpoint">Address of the client that sent the request</param>
/// <returns>The message to be sent back to the client or null</returns>
protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoint endpoint)
{
// In this case we don't do anything with the request
// but other discovery implementations might want to use the data
// in there, This way the client can ask for
// specific game mode or something
try
{
// this is an example reply message, return your own
// to include whatever is relevant for your game
return new ServerResponse
{
serverId = ServerId,
uri = transport.ServerUri()
};
}
catch (NotImplementedException)
{
Debug.LogError($"Transport {transport} does not support network discovery");
throw;
}
}
#endregion
#region Client
/// <summary>
/// Create a message that will be broadcasted on the network to discover servers
/// </summary>
/// <remarks>
/// Override if you wish to include additional data in the discovery message
/// such as desired game mode, language, difficulty, etc... </remarks>
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
protected override ServerRequest GetRequest() => new ServerRequest();
/// <summary>
/// Process the answer from a server
/// </summary>
/// <remarks>
/// A client receives a reply from a server, this method processes the
/// reply and raises an event
/// </remarks>
/// <param name="response">Response that came from the server</param>
/// <param name="endpoint">Address of the server that replied</param>
protected override void ProcessResponse(ServerResponse response, IPEndPoint endpoint)
{
// we received a message from the remote endpoint
response.EndPoint = endpoint;
// although we got a supposedly valid url, we may not be able to resolve
// the provided host
// However we know the real ip address of the server because we just
// received a packet from it, so use that as host.
UriBuilder realUri = new UriBuilder(response.uri)
{
Host = response.EndPoint.Address.ToString()
};
response.uri = realUri.Uri;
OnServerFound.Invoke(response);
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c761308e733c51245b2e8bb4201f46dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,396 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using UnityEngine;
// Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery
// forked from https://github.com/in0finite/MirrorNetworkDiscovery
// Both are MIT Licensed
namespace Mirror.Discovery
{
/// <summary>
/// Base implementation for Network Discovery. Extend this component
/// to provide custom discovery with game specific data
/// <see cref="NetworkDiscovery">NetworkDiscovery</see> for a sample implementation
/// </summary>
[DisallowMultipleComponent]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
where Request : NetworkMessage
where Response : NetworkMessage
{
public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
// each game should have a random unique handshake, this way you can tell if this is the same game or not
[HideInInspector]
public long secretHandshake;
[SerializeField]
[Tooltip("The UDP port the server will listen for multi-cast messages")]
protected int serverBroadcastListenPort = 47777;
[SerializeField]
[Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
public bool enableActiveDiscovery = true;
[SerializeField]
[Tooltip("Time in seconds between multi-cast messages")]
[Range(1, 60)]
float ActiveDiscoveryInterval = 3;
protected UdpClient serverUdpClient;
protected UdpClient clientUdpClient;
#if UNITY_EDITOR
void OnValidate()
{
if (secretHandshake == 0)
{
secretHandshake = RandomLong();
UnityEditor.Undo.RecordObject(this, "Set secret handshake");
}
}
#endif
public static long RandomLong()
{
int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
return value1 + ((long)value2 << 32);
}
/// <summary>
/// virtual so that inheriting classes' Start() can call base.Start() too
/// </summary>
public virtual void Start()
{
// Server mode? then start advertising
#if UNITY_SERVER
AdvertiseServer();
#endif
}
// Ensure the ports are cleared no matter when Game/Unity UI exits
void OnApplicationQuit()
{
//Debug.Log("NetworkDiscoveryBase OnApplicationQuit");
Shutdown();
}
void OnDisable()
{
//Debug.Log("NetworkDiscoveryBase OnDisable");
Shutdown();
}
void OnDestroy()
{
//Debug.Log("NetworkDiscoveryBase OnDestroy");
Shutdown();
}
void Shutdown()
{
if (serverUdpClient != null)
{
try
{
serverUdpClient.Close();
}
catch (Exception)
{
// it is just close, swallow the error
}
serverUdpClient = null;
}
if (clientUdpClient != null)
{
try
{
clientUdpClient.Close();
}
catch (Exception)
{
// it is just close, swallow the error
}
clientUdpClient = null;
}
CancelInvoke();
}
#region Server
/// <summary>
/// Advertise this server in the local network
/// </summary>
public void AdvertiseServer()
{
if (!SupportedOnThisPlatform)
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
StopDiscovery();
// Setup port -- may throw exception
serverUdpClient = new UdpClient(serverBroadcastListenPort)
{
EnableBroadcast = true,
MulticastLoopback = false
};
// listen for client pings
_ = ServerListenAsync();
}
public async Task ServerListenAsync()
{
while (true)
{
try
{
await ReceiveRequestAsync(serverUdpClient);
}
catch (ObjectDisposedException)
{
// socket has been closed
break;
}
catch (Exception)
{
}
}
}
async Task ReceiveRequestAsync(UdpClient udpClient)
{
// only proceed if there is available data in network buffer, or otherwise Receive() will block
// average time for UdpClient.Available : 10 us
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
{
long handshake = networkReader.ReadLong();
if (handshake != secretHandshake)
{
// message is not for us
throw new ProtocolViolationException("Invalid handshake");
}
Request request = networkReader.Read<Request>();
ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
}
}
/// <summary>
/// Reply to the client to inform it of this server
/// </summary>
/// <remarks>
/// Override if you wish to ignore server requests based on
/// custom criteria such as language, full server game mode or difficulty
/// </remarks>
/// <param name="request">Request coming from client</param>
/// <param name="endpoint">Address of the client that sent the request</param>
protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
{
Response info = ProcessRequest(request, endpoint);
if (info == null)
return;
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
{
try
{
writer.WriteLong(secretHandshake);
writer.Write(info);
ArraySegment<byte> data = writer.ToArraySegment();
// signature matches
// send response
serverUdpClient.Send(data.Array, data.Count, endpoint);
}
catch (Exception ex)
{
Debug.LogException(ex, this);
}
}
}
/// <summary>
/// Process the request from a client
/// </summary>
/// <remarks>
/// Override if you wish to provide more information to the clients
/// such as the name of the host player
/// </remarks>
/// <param name="request">Request coming from client</param>
/// <param name="endpoint">Address of the client that sent the request</param>
/// <returns>The message to be sent back to the client or null</returns>
protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
#endregion
#region Client
/// <summary>
/// Start Active Discovery
/// </summary>
public void StartDiscovery()
{
if (!SupportedOnThisPlatform)
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
StopDiscovery();
try
{
// Setup port
clientUdpClient = new UdpClient(0)
{
EnableBroadcast = true,
MulticastLoopback = false
};
}
catch (Exception)
{
// Free the port if we took it
//Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception");
Shutdown();
throw;
}
_ = ClientListenAsync();
if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
}
/// <summary>
/// Stop Active Discovery
/// </summary>
public void StopDiscovery()
{
//Debug.Log("NetworkDiscoveryBase StopDiscovery");
Shutdown();
}
/// <summary>
/// Awaits for server response
/// </summary>
/// <returns>ClientListenAsync Task</returns>
public async Task ClientListenAsync()
{
// while clientUpdClient to fix:
// https://github.com/vis2k/Mirror/pull/2908
//
// If, you cancel discovery the clientUdpClient is set to null.
// However, nothing cancels ClientListenAsync. If we change the if(true)
// to check if the client is null. You can properly cancel the discovery,
// and kill the listen thread.
//
// Prior to this fix, if you cancel the discovery search. It crashes the
// thread, and is super noisy in the output. As well as causes issues on
// the quest.
while (clientUdpClient != null)
{
try
{
await ReceiveGameBroadcastAsync(clientUdpClient);
}
catch (ObjectDisposedException)
{
// socket was closed, no problem
return;
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
}
/// <summary>
/// Sends discovery request from client
/// </summary>
public void BroadcastDiscoveryRequest()
{
if (clientUdpClient == null)
return;
if (NetworkClient.isConnected)
{
StopDiscovery();
return;
}
IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
{
writer.WriteLong(secretHandshake);
try
{
Request request = GetRequest();
writer.Write(request);
ArraySegment<byte> data = writer.ToArraySegment();
clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
}
catch (Exception)
{
// It is ok if we can't broadcast to one of the addresses
}
}
}
/// <summary>
/// Create a message that will be broadcasted on the network to discover servers
/// </summary>
/// <remarks>
/// Override if you wish to include additional data in the discovery message
/// such as desired game mode, language, difficulty, etc... </remarks>
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
protected virtual Request GetRequest() => default;
async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
{
// only proceed if there is available data in network buffer, or otherwise Receive() will block
// average time for UdpClient.Available : 10 us
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
{
if (networkReader.ReadLong() != secretHandshake)
return;
Response response = networkReader.Read<Response>();
ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
}
}
/// <summary>
/// Process the answer from a server
/// </summary>
/// <remarks>
/// A client receives a reply from a server, this method processes the
/// reply and raises an event
/// </remarks>
/// <param name="response">Response that came from the server</param>
/// <param name="endpoint">Address of the server that replied</param>
protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
#endregion
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f54db2e39d506634a9373c6d52305187
guid: b9971d60ce61f4e39b07cd9e7e0c68fa
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,132 @@
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.Discovery
{
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkDiscoveryHUD")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
[RequireComponent(typeof(NetworkDiscovery))]
public class NetworkDiscoveryHUD : MonoBehaviour
{
readonly Dictionary<long, ServerResponse> discoveredServers = new Dictionary<long, ServerResponse>();
Vector2 scrollViewPos = Vector2.zero;
public NetworkDiscovery networkDiscovery;
#if UNITY_EDITOR
void OnValidate()
{
if (networkDiscovery == null)
{
networkDiscovery = GetComponent<NetworkDiscovery>();
UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer);
UnityEditor.Undo.RecordObjects(new Object[] { this, networkDiscovery }, "Set NetworkDiscovery");
}
}
#endif
void OnGUI()
{
if (NetworkManager.singleton == null)
return;
if (!NetworkClient.isConnected && !NetworkServer.active && !NetworkClient.active)
DrawGUI();
if (NetworkServer.active || NetworkClient.active)
StopButtons();
}
void DrawGUI()
{
GUILayout.BeginArea(new Rect(10, 10, 300, 500));
GUILayout.BeginHorizontal();
if (GUILayout.Button("Find Servers"))
{
discoveredServers.Clear();
networkDiscovery.StartDiscovery();
}
// LAN Host
if (GUILayout.Button("Start Host"))
{
discoveredServers.Clear();
NetworkManager.singleton.StartHost();
networkDiscovery.AdvertiseServer();
}
// Dedicated server
if (GUILayout.Button("Start Server"))
{
discoveredServers.Clear();
NetworkManager.singleton.StartServer();
networkDiscovery.AdvertiseServer();
}
GUILayout.EndHorizontal();
// show list of found server
GUILayout.Label($"Discovered Servers [{discoveredServers.Count}]:");
// servers
scrollViewPos = GUILayout.BeginScrollView(scrollViewPos);
foreach (ServerResponse info in discoveredServers.Values)
if (GUILayout.Button(info.EndPoint.Address.ToString()))
Connect(info);
GUILayout.EndScrollView();
GUILayout.EndArea();
}
void StopButtons()
{
GUILayout.BeginArea(new Rect(10, 40, 100, 25));
// stop host if host mode
if (NetworkServer.active && NetworkClient.isConnected)
{
if (GUILayout.Button("Stop Host"))
{
NetworkManager.singleton.StopHost();
networkDiscovery.StopDiscovery();
}
}
// stop client if client-only
else if (NetworkClient.isConnected)
{
if (GUILayout.Button("Stop Client"))
{
NetworkManager.singleton.StopClient();
networkDiscovery.StopDiscovery();
}
}
// stop server if server-only
else if (NetworkServer.active)
{
if (GUILayout.Button("Stop Server"))
{
NetworkManager.singleton.StopServer();
networkDiscovery.StopDiscovery();
}
}
GUILayout.EndArea();
}
void Connect(ServerResponse info)
{
networkDiscovery.StopDiscovery();
NetworkManager.singleton.StartClient(info.uri);
}
public void OnDiscoveredServer(ServerResponse info)
{
// Note that you can check the versioning to decide if you can connect to the server or not using this method
discoveredServers[info.serverId] = info;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88c37d3deca7a834d80cfd8d3cfcc510
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
namespace Mirror.Discovery
{
public struct ServerRequest : NetworkMessage {}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c2e457ad00c1a044a924a5bc00776df9
guid: ea7254bf7b9454da4adad881d94cd141
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,18 @@
using System;
using System.Net;
namespace Mirror.Discovery
{
public struct ServerResponse : NetworkMessage
{
// The server that sent this
// this is a property so that it is not serialized, but the
// client fills this up after we receive it
public IPEndPoint EndPoint { get; set; }
public Uri uri;
// Prevent duplicate server appearance when a connection can be made via LAN on multiple NICs
public long serverId;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36f97227fdf2d7a4e902db5bfc43039c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bfbf2a1f2b300c5489dcab219ef2846e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,93 @@
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/Experimental/NetworkLerpRigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")]
public class NetworkLerpRigidbody : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody target = null;
[Tooltip("How quickly current velocity approaches target velocity")]
[SerializeField] float lerpVelocityAmount = 0.5f;
[Tooltip("How quickly current position approaches target position")]
[SerializeField] float lerpPositionAmount = 0.5f;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
[SerializeField] bool clientAuthority = false;
float nextSyncTime;
[SyncVar()]
Vector3 targetVelocity;
[SyncVar()]
Vector3 targetPosition;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
/// <returns></returns>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && hasAuthority;
void OnValidate()
{
if (target == null)
{
target = GetComponent<Rigidbody>();
}
}
void Update()
{
if (isServer)
{
SyncToClients();
}
else if (ClientWithAuthority)
{
SendToServer();
}
}
void SyncToClients()
{
targetVelocity = target.velocity;
targetPosition = target.position;
}
void SendToServer()
{
float now = Time.time;
if (now > nextSyncTime)
{
nextSyncTime = now + syncInterval;
CmdSendState(target.velocity, target.position);
}
}
[Command]
void CmdSendState(Vector3 velocity, Vector3 position)
{
target.velocity = velocity;
target.position = position;
targetVelocity = velocity;
targetPosition = position;
}
void FixedUpdate()
{
if (IgnoreSync) { return; }
target.velocity = Vector3.Lerp(target.velocity, targetVelocity, lerpVelocityAmount);
target.position = Vector3.Lerp(target.position, targetPosition, lerpPositionAmount);
// add velocity to position as position would have moved on server at that velocity
targetPosition += target.velocity * Time.fixedDeltaTime;
// TODO does this also need to sync acceleration so and update velocity?
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f032128052c95a46afb0ddd97d994cc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,361 @@
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/Experimental/NetworkRigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
public class NetworkRigidbody : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody target = null;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority = false;
[Header("Velocity")]
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
[SerializeField] bool clearVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float velocitySensitivity = 0.1f;
[Header("Angular Velocity")]
[Tooltip("Syncs AngularVelocity every SyncInterval")]
[SerializeField] bool syncAngularVelocity = true;
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
[SerializeField] bool clearAngularVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float angularVelocitySensitivity = 0.1f;
/// <summary>
/// Values sent on client with authority after they are sent to the server
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
void OnValidate()
{
if (target == null)
{
target = GetComponent<Rigidbody>();
}
}
#region Sync vars
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector3 velocity;
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
Vector3 angularVelocity;
[SyncVar(hook = nameof(OnIsKinematicChanged))]
bool isKinematic;
[SyncVar(hook = nameof(OnUseGravityChanged))]
bool useGravity;
[SyncVar(hook = nameof(OnuDragChanged))]
float drag;
[SyncVar(hook = nameof(OnAngularDragChanged))]
float angularDrag;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
/// <returns></returns>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && hasAuthority;
void OnVelocityChanged(Vector3 _, Vector3 newValue)
{
if (IgnoreSync)
return;
target.velocity = newValue;
}
void OnAngularVelocityChanged(Vector3 _, Vector3 newValue)
{
if (IgnoreSync)
return;
target.angularVelocity = newValue;
}
void OnIsKinematicChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.isKinematic = newValue;
}
void OnUseGravityChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.useGravity = newValue;
}
void OnuDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.drag = newValue;
}
void OnAngularDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularDrag = newValue;
}
#endregion
internal void Update()
{
if (isServer)
{
SyncToClients();
}
else if (ClientWithAuthority)
{
SendToServer();
}
}
internal void FixedUpdate()
{
if (clearAngularVelocity && !syncAngularVelocity)
{
target.angularVelocity = Vector3.zero;
}
if (clearVelocity && !syncVelocity)
{
target.velocity = Vector3.zero;
}
}
/// <summary>
/// Updates sync var values on server so that they sync to the client
/// </summary>
[Server]
void SyncToClients()
{
// only update if they have changed more than Sensitivity
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
if (velocityChanged)
{
velocity = currentVelocity;
previousValue.velocity = currentVelocity;
}
if (angularVelocityChanged)
{
angularVelocity = currentAngularVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
// other rigidbody settings
isKinematic = target.isKinematic;
useGravity = target.useGravity;
drag = target.drag;
angularDrag = target.angularDrag;
}
/// <summary>
/// Uses Command to send values to server
/// </summary>
[Client]
void SendToServer()
{
if (!hasAuthority)
{
Debug.LogWarning("SendToServer called without authority");
return;
}
SendVelocity();
SendRigidBodySettings();
}
[Client]
void SendVelocity()
{
float now = Time.time;
if (now < previousValue.nextSyncTime)
return;
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
// however if only velocity has changed just send velocity
if (angularVelocityChanged)
{
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
previousValue.velocity = currentVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
else if (velocityChanged)
{
CmdSendVelocity(currentVelocity);
previousValue.velocity = currentVelocity;
}
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
{
previousValue.nextSyncTime = now + syncInterval;
}
}
[Client]
void SendRigidBodySettings()
{
// These shouldn't change often so it is ok to send in their own Command
if (previousValue.isKinematic != target.isKinematic)
{
CmdSendIsKinematic(target.isKinematic);
previousValue.isKinematic = target.isKinematic;
}
if (previousValue.useGravity != target.useGravity)
{
CmdSendUseGravity(target.useGravity);
previousValue.useGravity = target.useGravity;
}
if (previousValue.drag != target.drag)
{
CmdSendDrag(target.drag);
previousValue.drag = target.drag;
}
if (previousValue.angularDrag != target.angularDrag)
{
CmdSendAngularDrag(target.angularDrag);
previousValue.angularDrag = target.angularDrag;
}
}
/// <summary>
/// Called when only Velocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocity(Vector3 velocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.velocity = velocity;
target.velocity = velocity;
}
/// <summary>
/// Called when angularVelocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
if (syncVelocity)
{
this.velocity = velocity;
target.velocity = velocity;
}
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
}
[Command]
void CmdSendIsKinematic(bool isKinematic)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.isKinematic = isKinematic;
target.isKinematic = isKinematic;
}
[Command]
void CmdSendUseGravity(bool useGravity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.useGravity = useGravity;
target.useGravity = useGravity;
}
[Command]
void CmdSendDrag(float drag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.drag = drag;
target.drag = drag;
}
[Command]
void CmdSendAngularDrag(float angularDrag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.angularDrag = angularDrag;
target.angularDrag = angularDrag;
}
/// <summary>
/// holds previously synced values
/// </summary>
public class ClientSyncState
{
/// <summary>
/// Next sync time that velocity will be synced, based on syncInterval.
/// </summary>
public float nextSyncTime;
public Vector3 velocity;
public Vector3 angularVelocity;
public bool isKinematic;
public bool useGravity;
public float drag;
public float angularDrag;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 83392ae5c1b731446909f252fd494ae4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,360 @@
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/Experimental/NetworkRigidbody2D")]
public class NetworkRigidbody2D : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody2D target = null;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority = false;
[Header("Velocity")]
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
[SerializeField] bool clearVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float velocitySensitivity = 0.1f;
[Header("Angular Velocity")]
[Tooltip("Syncs AngularVelocity every SyncInterval")]
[SerializeField] bool syncAngularVelocity = true;
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
[SerializeField] bool clearAngularVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float angularVelocitySensitivity = 0.1f;
/// <summary>
/// Values sent on client with authority after they are sent to the server
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
void OnValidate()
{
if (target == null)
{
target = GetComponent<Rigidbody2D>();
}
}
#region Sync vars
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector2 velocity;
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
float angularVelocity;
[SyncVar(hook = nameof(OnIsKinematicChanged))]
bool isKinematic;
[SyncVar(hook = nameof(OnGravityScaleChanged))]
float gravityScale;
[SyncVar(hook = nameof(OnuDragChanged))]
float drag;
[SyncVar(hook = nameof(OnAngularDragChanged))]
float angularDrag;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
/// <returns></returns>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && hasAuthority;
void OnVelocityChanged(Vector2 _, Vector2 newValue)
{
if (IgnoreSync)
return;
target.velocity = newValue;
}
void OnAngularVelocityChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularVelocity = newValue;
}
void OnIsKinematicChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.isKinematic = newValue;
}
void OnGravityScaleChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.gravityScale = newValue;
}
void OnuDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.drag = newValue;
}
void OnAngularDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularDrag = newValue;
}
#endregion
internal void Update()
{
if (isServer)
{
SyncToClients();
}
else if (ClientWithAuthority)
{
SendToServer();
}
}
internal void FixedUpdate()
{
if (clearAngularVelocity && !syncAngularVelocity)
{
target.angularVelocity = 0f;
}
if (clearVelocity && !syncVelocity)
{
target.velocity = Vector2.zero;
}
}
/// <summary>
/// Updates sync var values on server so that they sync to the client
/// </summary>
[Server]
void SyncToClients()
{
// only update if they have changed more than Sensitivity
Vector2 currentVelocity = syncVelocity ? target.velocity : default;
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity) > angularVelocitySensitivity);
if (velocityChanged)
{
velocity = currentVelocity;
previousValue.velocity = currentVelocity;
}
if (angularVelocityChanged)
{
angularVelocity = currentAngularVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
// other rigidbody settings
isKinematic = target.isKinematic;
gravityScale = target.gravityScale;
drag = target.drag;
angularDrag = target.angularDrag;
}
/// <summary>
/// Uses Command to send values to server
/// </summary>
[Client]
void SendToServer()
{
if (!hasAuthority)
{
Debug.LogWarning("SendToServer called without authority");
return;
}
SendVelocity();
SendRigidBodySettings();
}
[Client]
void SendVelocity()
{
float now = Time.time;
if (now < previousValue.nextSyncTime)
return;
Vector2 currentVelocity = syncVelocity ? target.velocity : default;
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && previousValue.angularVelocity != currentAngularVelocity;//((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
// however if only velocity has changed just send velocity
if (angularVelocityChanged)
{
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
previousValue.velocity = currentVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
else if (velocityChanged)
{
CmdSendVelocity(currentVelocity);
previousValue.velocity = currentVelocity;
}
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
{
previousValue.nextSyncTime = now + syncInterval;
}
}
[Client]
void SendRigidBodySettings()
{
// These shouldn't change often so it is ok to send in their own Command
if (previousValue.isKinematic != target.isKinematic)
{
CmdSendIsKinematic(target.isKinematic);
previousValue.isKinematic = target.isKinematic;
}
if (previousValue.gravityScale != target.gravityScale)
{
CmdChangeGravityScale(target.gravityScale);
previousValue.gravityScale = target.gravityScale;
}
if (previousValue.drag != target.drag)
{
CmdSendDrag(target.drag);
previousValue.drag = target.drag;
}
if (previousValue.angularDrag != target.angularDrag)
{
CmdSendAngularDrag(target.angularDrag);
previousValue.angularDrag = target.angularDrag;
}
}
/// <summary>
/// Called when only Velocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocity(Vector2 velocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.velocity = velocity;
target.velocity = velocity;
}
/// <summary>
/// Called when angularVelocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
if (syncVelocity)
{
this.velocity = velocity;
target.velocity = velocity;
}
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
}
[Command]
void CmdSendIsKinematic(bool isKinematic)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.isKinematic = isKinematic;
target.isKinematic = isKinematic;
}
[Command]
void CmdChangeGravityScale(float gravityScale)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.gravityScale = gravityScale;
target.gravityScale = gravityScale;
}
[Command]
void CmdSendDrag(float drag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.drag = drag;
target.drag = drag;
}
[Command]
void CmdSendAngularDrag(float angularDrag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.angularDrag = angularDrag;
target.angularDrag = angularDrag;
}
/// <summary>
/// holds previously synced values
/// </summary>
public class ClientSyncState
{
/// <summary>
/// Next sync time that velocity will be synced, based on syncInterval.
/// </summary>
public float nextSyncTime;
public Vector2 velocity;
public float angularVelocity;
public bool isKinematic;
public float gravityScale;
public float drag;
public float angularDrag;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab2cbc52526ea384ba280d13cd1a57b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using UnityEngine;
namespace Mirror.Experimental
{
[DisallowMultipleComponent]
[AddComponentMenu("Network/Experimental/NetworkTransformExperimental")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform")]
public class NetworkTransform : NetworkTransformBase
{
protected override Transform targetTransform => transform;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 741bbe11f5357b44593b15c0d11b16bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,529 @@
// vis2k:
// base class for NetworkTransform and NetworkTransformChild.
// New method is simple and stupid. No more 1500 lines of code.
//
// Server sends current data.
// Client saves it and interpolates last and latest data points.
// Update handles transform movement / rotation
// FixedUpdate handles rigidbody movement / rotation
//
// Notes:
// * Built-in Teleport detection in case of lags / teleport / obstacles
// * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp
// * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code.
// * Initial delay might happen if server sends packet immediately after moving
// just 1cm, hence we move 1cm and then wait 100ms for next packet
// * Only way for smooth movement is to use a fixed movement speed during
// interpolation. interpolation over time is never that good.
//
using System;
using UnityEngine;
namespace Mirror.Experimental
{
public abstract class NetworkTransformBase : NetworkBehaviour
{
// target transform to sync. can be on a child.
protected abstract Transform targetTransform { get; }
[Header("Authority")]
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
[SyncVar]
public bool clientAuthority;
[Tooltip("Set to true if updates from server should be ignored by owner")]
[SyncVar]
public bool excludeOwnerUpdate = true;
[Header("Synchronization")]
[Tooltip("Set to true if position should be synchronized")]
[SyncVar]
public bool syncPosition = true;
[Tooltip("Set to true if rotation should be synchronized")]
[SyncVar]
public bool syncRotation = true;
[Tooltip("Set to true if scale should be synchronized")]
[SyncVar]
public bool syncScale = true;
[Header("Interpolation")]
[Tooltip("Set to true if position should be interpolated")]
[SyncVar]
public bool interpolatePosition = true;
[Tooltip("Set to true if rotation should be interpolated")]
[SyncVar]
public bool interpolateRotation = true;
[Tooltip("Set to true if scale should be interpolated")]
[SyncVar]
public bool interpolateScale = true;
// Sensitivity is added for VR where human players tend to have micro movements so this can quiet down
// the network traffic. Additionally, rigidbody drift should send less traffic, e.g very slow sliding / rolling.
[Header("Sensitivity")]
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
[SyncVar]
public float localPositionSensitivity = .01f;
[Tooltip("If rotation exceeds this angle, it will be transmitted on the network")]
[SyncVar]
public float localRotationSensitivity = .01f;
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
[SyncVar]
public float localScaleSensitivity = .01f;
[Header("Diagnostics")]
// server
public Vector3 lastPosition;
public Quaternion lastRotation;
public Vector3 lastScale;
// client
// use local position/rotation for VR support
[Serializable]
public struct DataPoint
{
public float timeStamp;
public Vector3 localPosition;
public Quaternion localRotation;
public Vector3 localScale;
public float movementSpeed;
public bool isValid => timeStamp != 0;
}
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
bool IsOwnerWithClientAuthority => hasAuthority && clientAuthority;
// interpolation start and goal
public DataPoint start = new DataPoint();
public DataPoint goal = new DataPoint();
// We need to store this locally on the server so clients can't request Authority when ever they like
bool clientAuthorityBeforeTeleport;
void FixedUpdate()
{
// if server then always sync to others.
// let the clients know that this has moved
if (isServer && HasEitherMovedRotatedScaled())
{
ServerUpdate();
}
if (isClient)
{
// send to server if we have local authority (and aren't the server)
// -> only if connectionToServer has been initialized yet too
if (IsOwnerWithClientAuthority)
{
ClientAuthorityUpdate();
}
else if (goal.isValid)
{
ClientRemoteUpdate();
}
}
}
void ServerUpdate()
{
RpcMove(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
}
void ClientAuthorityUpdate()
{
if (!isServer && HasEitherMovedRotatedScaled())
{
// serialize
// local position/rotation for VR support
// send to server
CmdClientToServerSync(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
}
}
void ClientRemoteUpdate()
{
// teleport or interpolate
if (NeedsTeleport())
{
// local position/rotation for VR support
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
// reset data points so we don't keep interpolating
start = new DataPoint();
goal = new DataPoint();
}
else
{
// local position/rotation for VR support
ApplyPositionRotationScale(InterpolatePosition(start, goal, targetTransform.localPosition),
InterpolateRotation(start, goal, targetTransform.localRotation),
InterpolateScale(start, goal, targetTransform.localScale));
}
}
// moved or rotated or scaled since last time we checked it?
bool HasEitherMovedRotatedScaled()
{
// Save last for next frame to compare only if change was detected, otherwise
// slow moving objects might never sync because of C#'s float comparison tolerance.
// See also: https://github.com/vis2k/Mirror/pull/428)
bool changed = HasMoved || HasRotated || HasScaled;
if (changed)
{
// local position/rotation for VR support
if (syncPosition) lastPosition = targetTransform.localPosition;
if (syncRotation) lastRotation = targetTransform.localRotation;
if (syncScale) lastScale = targetTransform.localScale;
}
return changed;
}
// local position/rotation for VR support
// SqrMagnitude is faster than Distance per Unity docs
// https://docs.unity3d.com/ScriptReference/Vector3-sqrMagnitude.html
bool HasMoved => syncPosition && Vector3.SqrMagnitude(lastPosition - targetTransform.localPosition) > localPositionSensitivity * localPositionSensitivity;
bool HasRotated => syncRotation && Quaternion.Angle(lastRotation, targetTransform.localRotation) > localRotationSensitivity;
bool HasScaled => syncScale && Vector3.SqrMagnitude(lastScale - targetTransform.localScale) > localScaleSensitivity * localScaleSensitivity;
// teleport / lag / stuck detection
// - checking distance is not enough since there could be just a tiny fence between us and the goal
// - checking time always works, this way we just teleport if we still didn't reach the goal after too much time has elapsed
bool NeedsTeleport()
{
// calculate time between the two data points
float startTime = start.isValid ? start.timeStamp : Time.time - Time.fixedDeltaTime;
float goalTime = goal.isValid ? goal.timeStamp : Time.time;
float difference = goalTime - startTime;
float timeSinceGoalReceived = Time.time - goalTime;
return timeSinceGoalReceived > difference * 5;
}
// local authority client sends sync message to server for broadcasting
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSync(Vector3 position, uint packedRotation, Vector3 scale)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// deserialize payload
SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
// server-only mode does no interpolation to save computations, but let's set the position directly
if (isServer && !isClient)
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
RpcMove(position, packedRotation, scale);
}
[ClientRpc(channel = Channels.Unreliable)]
void RpcMove(Vector3 position, uint packedRotation, Vector3 scale)
{
if (hasAuthority && excludeOwnerUpdate) return;
if (!isServer)
SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
}
// serialization is needed by OnSerialize and by manual sending from authority
void SetGoal(Vector3 position, Quaternion rotation, Vector3 scale)
{
// put it into a data point immediately
DataPoint temp = new DataPoint
{
// deserialize position
localPosition = position,
localRotation = rotation,
localScale = scale,
timeStamp = Time.time
};
// movement speed: based on how far it moved since last time has to be calculated before 'start' is overwritten
temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetTransform, Time.fixedDeltaTime);
// reassign start wisely
// first ever data point? then make something up for previous one so that we can start interpolation without waiting for next.
if (start.timeStamp == 0)
{
start = new DataPoint
{
timeStamp = Time.time - Time.fixedDeltaTime,
// local position/rotation for VR support
localPosition = targetTransform.localPosition,
localRotation = targetTransform.localRotation,
localScale = targetTransform.localScale,
movementSpeed = temp.movementSpeed
};
}
// second or nth data point? then update previous
// but: we start at where ever we are right now, so that it's perfectly smooth and we don't jump anywhere
//
// example if we are at 'x':
//
// A--x->B
//
// and then receive a new point C:
//
// A--x--B
// |
// |
// C
//
// then we don't want to just jump to B and start interpolation:
//
// x
// |
// |
// C
//
// we stay at 'x' and interpolate from there to C:
//
// x..B
// \ .
// \.
// C
//
else
{
float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition);
float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition);
start = goal;
// local position/rotation for VR support
// teleport / lag / obstacle detection: only continue at current position if we aren't too far away
// XC < AB + BC (see comments above)
if (Vector3.Distance(targetTransform.localPosition, start.localPosition) < oldDistance + newDistance)
{
start.localPosition = targetTransform.localPosition;
start.localRotation = targetTransform.localRotation;
start.localScale = targetTransform.localScale;
}
}
// set new destination in any case. new data is best data.
goal = temp;
}
// try to estimate movement speed for a data point based on how far it moved since the previous one
// - if this is the first time ever then we use our best guess:
// - delta based on transform.localPosition
// - elapsed based on send interval hoping that it roughly matches
static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval)
{
Vector3 delta = to.localPosition - (from.localPosition != transform.localPosition ? from.localPosition : transform.localPosition);
float elapsed = from.isValid ? to.timeStamp - from.timeStamp : sendInterval;
// avoid NaN
return elapsed > 0 ? delta.magnitude / elapsed : 0;
}
// set position carefully depending on the target component
void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale)
{
// local position/rotation for VR support
if (syncPosition) targetTransform.localPosition = position;
if (syncRotation) targetTransform.localRotation = rotation;
if (syncScale) targetTransform.localScale = scale;
}
// where are we in the timeline between start and goal? [0,1]
Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition)
{
if (!interpolatePosition)
return currentPosition;
if (start.movementSpeed != 0)
{
// Option 1: simply interpolate based on time, but stutter will happen, it's not that smooth.
// This is especially noticeable if the camera automatically follows the player
// - Tell SonarCloud this isn't really commented code but actual comments and to stfu about it
// - float t = CurrentInterpolationFactor();
// - return Vector3.Lerp(start.position, goal.position, t);
// Option 2: always += speed
// speed is 0 if we just started after idle, so always use max for best results
float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed);
return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime);
}
return currentPosition;
}
Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation)
{
if (!interpolateRotation)
return defaultRotation;
if (start.localRotation != goal.localRotation)
{
float t = CurrentInterpolationFactor(start, goal);
return Quaternion.Slerp(start.localRotation, goal.localRotation, t);
}
return defaultRotation;
}
Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale)
{
if (!interpolateScale)
return currentScale;
if (start.localScale != goal.localScale)
{
float t = CurrentInterpolationFactor(start, goal);
return Vector3.Lerp(start.localScale, goal.localScale, t);
}
return currentScale;
}
static float CurrentInterpolationFactor(DataPoint start, DataPoint goal)
{
if (start.isValid)
{
float difference = goal.timeStamp - start.timeStamp;
// the moment we get 'goal', 'start' is supposed to start, so elapsed time is based on:
float elapsed = Time.time - goal.timeStamp;
// avoid NaN
return difference > 0 ? elapsed / difference : 1;
}
return 1;
}
#region Server Teleport (force move player)
/// <summary>
/// This method will override this GameObject's current Transform.localPosition to the specified Vector3 and update all clients.
/// <para>NOTE: position must be in LOCAL space if the transform has a parent</para>
/// </summary>
/// <param name="localPosition">Where to teleport this GameObject</param>
[Server]
public void ServerTeleport(Vector3 localPosition)
{
Quaternion localRotation = targetTransform.localRotation;
ServerTeleport(localPosition, localRotation);
}
/// <summary>
/// This method will override this GameObject's current Transform.localPosition and Transform.localRotation
/// to the specified Vector3 and Quaternion and update all clients.
/// <para>NOTE: localPosition must be in LOCAL space if the transform has a parent</para>
/// <para>NOTE: localRotation must be in LOCAL space if the transform has a parent</para>
/// </summary>
/// <param name="localPosition">Where to teleport this GameObject</param>
/// <param name="localRotation">Which rotation to set this GameObject</param>
[Server]
public void ServerTeleport(Vector3 localPosition, Quaternion localRotation)
{
// To prevent applying the position updates received from client (if they have ClientAuth) while being teleported.
// clientAuthorityBeforeTeleport defaults to false when not teleporting, if it is true then it means that teleport
// was previously called but not finished therefore we should keep it as true so that 2nd teleport call doesn't clear authority
clientAuthorityBeforeTeleport = clientAuthority || clientAuthorityBeforeTeleport;
clientAuthority = false;
DoTeleport(localPosition, localRotation);
// tell all clients about new values
RpcTeleport(localPosition, Compression.CompressQuaternion(localRotation), clientAuthorityBeforeTeleport);
}
void DoTeleport(Vector3 newLocalPosition, Quaternion newLocalRotation)
{
targetTransform.localPosition = newLocalPosition;
targetTransform.localRotation = newLocalRotation;
// Since we are overriding the position we don't need a goal and start.
// Reset them to null for fresh start
goal = new DataPoint();
start = new DataPoint();
lastPosition = newLocalPosition;
lastRotation = newLocalRotation;
}
[ClientRpc(channel = Channels.Unreliable)]
void RpcTeleport(Vector3 newPosition, uint newPackedRotation, bool isClientAuthority)
{
DoTeleport(newPosition, Compression.DecompressQuaternion(newPackedRotation));
// only send finished if is owner and is ClientAuthority on server
if (hasAuthority && isClientAuthority)
CmdTeleportFinished();
}
/// <summary>
/// This RPC will be invoked on server after client finishes overriding the position.
/// </summary>
/// <param name="initialAuthority"></param>
[Command(channel = Channels.Unreliable)]
void CmdTeleportFinished()
{
if (clientAuthorityBeforeTeleport)
{
clientAuthority = true;
// reset value so doesn't effect future calls, see note in ServerTeleport
clientAuthorityBeforeTeleport = false;
}
else
{
Debug.LogWarning("Client called TeleportFinished when clientAuthority was false on server", this);
}
}
#endregion
#region Debug Gizmos
// draw the data points for easier debugging
void OnDrawGizmos()
{
// draw start and goal points and a line between them
if (start.localPosition != goal.localPosition)
{
DrawDataPointGizmo(start, Color.yellow);
DrawDataPointGizmo(goal, Color.green);
DrawLineBetweenDataPoints(start, goal, Color.cyan);
}
}
static void DrawDataPointGizmo(DataPoint data, Color color)
{
// use a little offset because transform.localPosition might be in the ground in many cases
Vector3 offset = Vector3.up * 0.01f;
// draw position
Gizmos.color = color;
Gizmos.DrawSphere(data.localPosition + offset, 0.5f);
// draw forward and up like unity move tool
Gizmos.color = Color.blue;
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward);
Gizmos.color = Color.green;
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up);
}
static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color)
{
Gizmos.color = color;
Gizmos.DrawLine(data1.localPosition, data2.localPosition);
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea7c690c4fbf8c4439726f4c62eda6d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
using UnityEngine;
namespace Mirror.Experimental
{
/// <summary>
/// A component to synchronize the position of child transforms of networked objects.
/// <para>There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the received values.</para>
/// </summary>
[AddComponentMenu("Network/Experimental/NetworkTransformChildExperimentalExperimental")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform-child")]
public class NetworkTransformChild : NetworkTransformBase
{
[Header("Target")]
public Transform target;
protected override Transform targetTransform => target;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f65214da13a861f4a8ae309d3daea1c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,112 @@
// People should be able to see and report errors to the developer very easily.
//
// Unity's Developer Console only works in development builds and it only shows
// errors. This class provides a console that works in all builds and also shows
// log and warnings in development builds.
//
// Note: we don't include the stack trace, because that can also be grabbed from
// the log files if needed.
//
// Note: there is no 'hide' button because we DO want people to see those errors
// and report them back to us.
//
// Note: normal Debug.Log messages can be shown by building in Debug/Development
// mode.
using UnityEngine;
using System.Collections.Generic;
namespace Mirror
{
struct LogEntry
{
public string message;
public LogType type;
public LogEntry(string message, LogType type)
{
this.message = message;
this.type = type;
}
}
public class GUIConsole : MonoBehaviour
{
public int height = 150;
// only keep the recent 'n' entries. otherwise memory would grow forever
// and drawing would get slower and slower.
public int maxLogCount = 50;
// log as queue so we can remove the first entry easily
Queue<LogEntry> log = new Queue<LogEntry>();
// hotkey to show/hide at runtime for easier debugging
// (sometimes we need to temporarily hide/show it)
// => F12 makes sense. nobody can find ^ in other games.
public KeyCode hotKey = KeyCode.F12;
// GUI
bool visible;
Vector2 scroll = Vector2.zero;
void Awake()
{
Application.logMessageReceived += OnLog;
}
// OnLog logs everything, even Debug.Log messages in release builds
// => this makes a lot of things easier. e.g. addon initialization logs.
// => it's really better to have than not to have those
void OnLog(string message, string stackTrace, LogType type)
{
// is this important?
bool isImportant = type == LogType.Error || type == LogType.Exception;
// use stack trace only if important
// (otherwise users would have to find and search the log file.
// seeing it in the console directly is way easier to deal with.)
// => only add \n if stack trace is available (only in debug builds)
if (isImportant && !string.IsNullOrWhiteSpace(stackTrace))
message += $"\n{stackTrace}";
// add to queue
log.Enqueue(new LogEntry(message, type));
// respect max entries
if (log.Count > maxLogCount)
log.Dequeue();
// become visible if it was important
// (no need to become visible for regular log. let the user decide.)
if (isImportant)
visible = true;
// auto scroll
scroll.y = float.MaxValue;
}
void Update()
{
if (Input.GetKeyDown(hotKey))
visible = !visible;
}
void OnGUI()
{
if (!visible) return;
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width), GUILayout.Height(height));
foreach (LogEntry entry in log)
{
if (entry.type == LogType.Error || entry.type == LogType.Exception)
GUI.color = Color.red;
else if (entry.type == LogType.Warning)
GUI.color = Color.yellow;
GUILayout.Label(entry.message);
GUI.color = Color.white;
}
GUILayout.EndScrollView();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9021b6cc314944290986ab6feb48db79
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c66f27e006ab94253b39a55a3b213651
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa4cbc6b9c584db4971985cb9f369077
timeCreated: 1613110605

View File

@@ -0,0 +1,69 @@
// straight forward Vector3.Distance based interest management.
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public class DistanceInterestManagement : InterestManagement
{
[Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")]
public int visRange = 10;
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
public float rebuildInterval = 1;
double lastRebuildTime;
// helper function to get vis range for a given object, or default.
int GetVisRange(NetworkIdentity identity)
{
DistanceInterestManagementCustomRange custom = identity.GetComponent<DistanceInterestManagementCustomRange>();
return custom != null ? custom.visRange : visRange;
}
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
{
int range = GetVisRange(identity);
return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers, bool initialize)
{
// cache range and .transform because both call GetComponent.
int range = GetVisRange(identity);
Vector3 position = identity.transform.position;
// brute force distance check
// -> only player connections can be observers, so it's enough if we
// go through all connections instead of all spawned identities.
// -> compared to UNET's sphere cast checking, this one is orders of
// magnitude faster. if we have 10k monsters and run a sphere
// cast 10k times, we will see a noticeable lag even with physics
// layers. but checking to every connection is fast.
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
{
// authenticated and joined world with a player?
if (conn != null && conn.isAuthenticated && conn.identity != null)
{
// check distance
if (Vector3.Distance(conn.identity.transform.position, position) < range)
{
newObservers.Add(conn);
}
}
}
}
void Update()
{
// only on server
if (!NetworkServer.active) return;
// rebuild all spawned NetworkIdentity's observers every interval
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
{
RebuildAll();
lastRebuildTime = NetworkTime.localTime;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f60becab051427fbdd3c8ac9ab4712b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
// add this to NetworkIdentities for custom range if needed.
// only works with DistanceInterestManagement.
using UnityEngine;
namespace Mirror
{
[RequireComponent(typeof(NetworkIdentity))]
[DisallowMultipleComponent]
public class DistanceInterestManagementCustomRange : MonoBehaviour
{
[Tooltip("The maximum range that objects will be visible at.")]
public int visRange = 20;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2e242ee38a14076a39934172a19079b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5eca5245ae6bb460e9a92f7e14d5493a
timeCreated: 1622649517

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
namespace Mirror
{
public class MatchInterestManagement : InterestManagement
{
readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchObjects =
new Dictionary<Guid, HashSet<NetworkIdentity>>();
readonly Dictionary<NetworkIdentity, Guid> lastObjectMatch =
new Dictionary<NetworkIdentity, Guid>();
HashSet<Guid> dirtyMatches = new HashSet<Guid>();
public override void OnSpawned(NetworkIdentity identity)
{
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
return;
Guid currentMatch = networkMatch.matchId;
lastObjectMatch[identity] = currentMatch;
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
if (currentMatch == Guid.Empty)
return;
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
if (!matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects))
{
objects = new HashSet<NetworkIdentity>();
matchObjects.Add(currentMatch, objects);
}
objects.Add(identity);
}
public override void OnDestroyed(NetworkIdentity identity)
{
lastObjectMatch.TryGetValue(identity, out Guid currentMatch);
lastObjectMatch.Remove(identity);
if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
RebuildMatchObservers(currentMatch);
}
[ServerCallback]
void Update()
{
// for each spawned:
// if match changed:
// add previous to dirty
// add new to dirty
foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
{
// Ignore objects that don't have a NetworkMatch component
if (!netIdentity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
continue;
Guid newMatch = networkMatch.matchId;
lastObjectMatch.TryGetValue(netIdentity, out Guid currentMatch);
// Guid.Empty is never a valid matchId
// Nothing to do if matchId hasn't changed
if (newMatch == Guid.Empty || newMatch == currentMatch)
continue;
// Mark new/old matches as dirty so they get rebuilt
UpdateDirtyMatches(newMatch, currentMatch);
// This object is in a new match so observers in the prior match
// and the new match need to rebuild their respective observers lists.
UpdateMatchObjects(netIdentity, newMatch, currentMatch);
}
// rebuild all dirty matchs
foreach (Guid dirtyMatch in dirtyMatches)
RebuildMatchObservers(dirtyMatch);
dirtyMatches.Clear();
}
void UpdateDirtyMatches(Guid newMatch, Guid currentMatch)
{
// Guid.Empty is never a valid matchId
if (currentMatch != Guid.Empty)
dirtyMatches.Add(currentMatch);
dirtyMatches.Add(newMatch);
}
void UpdateMatchObjects(NetworkIdentity netIdentity, Guid newMatch, Guid currentMatch)
{
// Remove this object from the hashset of the match it just left
// Guid.Empty is never a valid matchId
if (currentMatch != Guid.Empty)
matchObjects[currentMatch].Remove(netIdentity);
// Set this to the new match this object just entered
lastObjectMatch[netIdentity] = newMatch;
// Make sure this new match is in the dictionary
if (!matchObjects.ContainsKey(newMatch))
matchObjects.Add(newMatch, new HashSet<NetworkIdentity>());
// Add this object to the hashset of the new match
matchObjects[newMatch].Add(netIdentity);
}
void RebuildMatchObservers(Guid matchId)
{
foreach (NetworkIdentity netIdentity in matchObjects[matchId])
if (netIdentity != null)
NetworkServer.RebuildObservers(netIdentity, false);
}
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
{
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch identityNetworkMatch))
return false;
if (!newObserver.identity.TryGetComponent<NetworkMatch>(out NetworkMatch newObserverNetworkMatch))
return false;
return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers, bool initialize)
{
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
return;
Guid matchId = networkMatch.matchId;
// Guid.Empty is never a valid matchId
if (matchId == Guid.Empty)
return;
if (!matchObjects.TryGetValue(matchId, out HashSet<NetworkIdentity> objects))
return;
// Add everything in the hashset for this object's current match
foreach (NetworkIdentity networkIdentity in objects)
if (networkIdentity != null && networkIdentity.connectionToClient != null)
newObservers.Add(networkIdentity.connectionToClient);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d09f5c8bf2f4747b7a9284ef5d9ce2a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7655d309a46a4bd4860edf964228b3f6
timeCreated: 1622649517

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
using UnityEngine.SceneManagement;
namespace Mirror
{
public class SceneInterestManagement : InterestManagement
{
// Use Scene instead of string scene.name because when additively
// loading multiples of a subscene the name won't be unique
readonly Dictionary<Scene, HashSet<NetworkIdentity>> sceneObjects =
new Dictionary<Scene, HashSet<NetworkIdentity>>();
readonly Dictionary<NetworkIdentity, Scene> lastObjectScene =
new Dictionary<NetworkIdentity, Scene>();
HashSet<Scene> dirtyScenes = new HashSet<Scene>();
public override void OnSpawned(NetworkIdentity identity)
{
Scene currentScene = identity.gameObject.scene;
lastObjectScene[identity] = currentScene;
// Debug.Log($"SceneInterestManagement.OnSpawned({identity.name}) currentScene: {currentScene}");
if (!sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects))
{
objects = new HashSet<NetworkIdentity>();
sceneObjects.Add(currentScene, objects);
}
objects.Add(identity);
}
public override void OnDestroyed(NetworkIdentity identity)
{
Scene currentScene = lastObjectScene[identity];
lastObjectScene.Remove(identity);
if (sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
RebuildSceneObservers(currentScene);
}
void Update()
{
// only on server
if (!NetworkServer.active) return;
// for each spawned:
// if scene changed:
// add previous to dirty
// add new to dirty
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
{
Scene currentScene = lastObjectScene[identity];
Scene newScene = identity.gameObject.scene;
if (newScene == currentScene) continue;
// Mark new/old scenes as dirty so they get rebuilt
dirtyScenes.Add(currentScene);
dirtyScenes.Add(newScene);
// This object is in a new scene so observers in the prior scene
// and the new scene need to rebuild their respective observers lists.
// Remove this object from the hashset of the scene it just left
sceneObjects[currentScene].Remove(identity);
// Set this to the new scene this object just entered
lastObjectScene[identity] = newScene;
// Make sure this new scene is in the dictionary
if (!sceneObjects.ContainsKey(newScene))
sceneObjects.Add(newScene, new HashSet<NetworkIdentity>());
// Add this object to the hashset of the new scene
sceneObjects[newScene].Add(identity);
}
// rebuild all dirty scenes
foreach (Scene dirtyScene in dirtyScenes)
{
RebuildSceneObservers(dirtyScene);
}
dirtyScenes.Clear();
}
void RebuildSceneObservers(Scene scene)
{
foreach (NetworkIdentity netIdentity in sceneObjects[scene])
if (netIdentity != null)
NetworkServer.RebuildObservers(netIdentity, false);
}
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
{
return identity.gameObject.scene == newObserver.identity.gameObject.scene;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers,
bool initialize)
{
if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet<NetworkIdentity> objects))
return;
// Add everything in the hashset for this object's current scene
foreach (NetworkIdentity networkIdentity in objects)
if (networkIdentity != null && networkIdentity.connectionToClient != null)
newObservers.Add(networkIdentity.connectionToClient);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b979f26c95d34324ba005bfacfa9c4fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cfa12b73503344d49b398b01bcb07967
timeCreated: 1613110634

View File

@@ -0,0 +1,88 @@
// Grid2D from uMMORPG: get/set values of type T at any point
// -> not named 'Grid' because Unity already has a Grid type. causes warnings.
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public class Grid2D<T>
{
// the grid
// note that we never remove old keys.
// => over time, HashSet<T>s will be allocated for every possible
// grid position in the world
// => Clear() doesn't clear them so we don't constantly reallocate the
// entries when populating the grid in every Update() call
// => makes the code a lot easier too
// => this is FINE because in the worst case, every grid position in the
// game world is filled with a player anyway!
Dictionary<Vector2Int, HashSet<T>> grid = new Dictionary<Vector2Int, HashSet<T>>();
// cache a 9 neighbor grid of vector2 offsets so we can use them more easily
Vector2Int[] neighbourOffsets =
{
Vector2Int.up,
Vector2Int.up + Vector2Int.left,
Vector2Int.up + Vector2Int.right,
Vector2Int.left,
Vector2Int.zero,
Vector2Int.right,
Vector2Int.down,
Vector2Int.down + Vector2Int.left,
Vector2Int.down + Vector2Int.right
};
// helper function so we can add an entry without worrying
public void Add(Vector2Int position, T value)
{
// initialize set in grid if it's not in there yet
if (!grid.TryGetValue(position, out HashSet<T> hashSet))
{
hashSet = new HashSet<T>();
grid[position] = hashSet;
}
// add to it
hashSet.Add(value);
}
// helper function to get set at position without worrying
// -> result is passed as parameter to avoid allocations
// -> result is not cleared before. this allows us to pass the HashSet from
// GetWithNeighbours and avoid .UnionWith which is very expensive.
void GetAt(Vector2Int position, HashSet<T> result)
{
// return the set at position
if (grid.TryGetValue(position, out HashSet<T> hashSet))
{
foreach (T entry in hashSet)
result.Add(entry);
}
}
// helper function to get at position and it's 8 neighbors without worrying
// -> result is passed as parameter to avoid allocations
public void GetWithNeighbours(Vector2Int position, HashSet<T> result)
{
// clear result first
result.Clear();
// add neighbours
foreach (Vector2Int offset in neighbourOffsets)
GetAt(position + offset, result);
}
// clear: clears the whole grid
// IMPORTANT: we already allocated HashSet<T>s and don't want to do
// reallocate every single update when we rebuild the grid.
// => so simply remove each position's entries, but keep
// every position in there
// => see 'grid' comments above!
// => named ClearNonAlloc to make it more obvious!
public void ClearNonAlloc()
{
foreach (HashSet<T> hashSet in grid.Values)
hashSet.Clear();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c5232a4d2854116a35d52b80ec07752
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,139 @@
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
// => 30x faster in initial tests
// => scales way higher
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public class SpatialHashingInterestManagement : InterestManagement
{
[Tooltip("The maximum range that objects will be visible at.")]
public int visRange = 30;
// if we see 8 neighbors then 1 entry is visRange/3
public int resolution => visRange / 3;
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
public float rebuildInterval = 1;
double lastRebuildTime;
public enum CheckMethod
{
XZ_FOR_3D,
XY_FOR_2D
}
[Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")]
public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D;
// debugging
public bool showSlider;
// the grid
Grid2D<NetworkConnection> grid = new Grid2D<NetworkConnection>();
// project 3d world position to grid position
Vector2Int ProjectToGrid(Vector3 position) =>
checkMethod == CheckMethod.XZ_FOR_3D
? Vector2Int.RoundToInt(new Vector2(position.x, position.z) / resolution)
: Vector2Int.RoundToInt(new Vector2(position.x, position.y) / resolution);
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
{
// calculate projected positions
Vector2Int projected = ProjectToGrid(identity.transform.position);
Vector2Int observerProjected = ProjectToGrid(newObserver.identity.transform.position);
// distance needs to be at max one of the 8 neighbors, which is
// 1 for the direct neighbors
// 1.41 for the diagonal neighbors (= sqrt(2))
// => use sqrMagnitude and '2' to avoid computations. same result.
return (projected - observerProjected).sqrMagnitude <= 2;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers, bool initialize)
{
// add everyone in 9 neighbour grid
// -> pass observers to GetWithNeighbours directly to avoid allocations
// and expensive .UnionWith computations.
Vector2Int current = ProjectToGrid(identity.transform.position);
grid.GetWithNeighbours(current, newObservers);
}
// update everyone's position in the grid
// (internal so we can update from tests)
internal void Update()
{
// only on server
if (!NetworkServer.active) return;
// NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL
// entities every INTERVAL. consider the other approach later.
// IMPORTANT: refresh grid every update!
// => newly spawned entities get observers assigned via
// OnCheckObservers. this can happen any time and we don't want
// them broadcast to old (moved or destroyed) connections.
// => players do move all the time. we want them to always be in the
// correct grid position.
// => note that the actual 'rebuildall' doesn't need to happen all
// the time.
// NOTE: consider refreshing grid only every 'interval' too. but not
// for now. stability & correctness matter.
// clear old grid results before we update everyone's position.
// (this way we get rid of destroyed connections automatically)
//
// NOTE: keeps allocated HashSets internally.
// clearing & populating every frame works without allocations
grid.ClearNonAlloc();
// put every connection into the grid at it's main player's position
// NOTE: player sees in a radius around him. NOT around his pet too.
foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values)
{
// authenticated and joined world with a player?
if (connection.isAuthenticated && connection.identity != null)
{
// calculate current grid position
Vector2Int position = ProjectToGrid(connection.identity.transform.position);
// put into grid
grid.Add(position, connection);
}
}
// rebuild all spawned entities' observers every 'interval'
// this will call OnRebuildObservers which then returns the
// observers at grid[position] for each entity.
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
{
RebuildAll();
lastRebuildTime = NetworkTime.localTime;
}
}
// OnGUI allocates even if it does nothing. avoid in release.
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// slider from dotsnet. it's nice to play around with in the benchmark
// demo.
void OnGUI()
{
if (!showSlider) return;
// only show while server is running. not on client, etc.
if (!NetworkServer.active) return;
int height = 30;
int width = 250;
GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height));
GUILayout.BeginHorizontal("Box");
GUILayout.Label("Radius:");
visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150)));
GUILayout.Label(visRange.ToString());
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39adc6e09d5544ed955a50ce8600355a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "Mirror.Components",
"references": [
"Mirror"
],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 72872094b21c16e48b631b2224833d49
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,634 @@
using System.Linq;
using UnityEngine;
using UnityEngine.Serialization;
namespace Mirror
{
/// <summary>
/// A component to synchronize Mecanim animation states for networked objects.
/// </summary>
/// <remarks>
/// <para>The animation of game objects can be networked by this component. There are two models of authority for networked movement:</para>
/// <para>If the object has authority on the client, then it should be animated locally on the owning client. The animation state information will be sent from the owning client to the server, then broadcast to all of the other clients. This is common for player objects.</para>
/// <para>If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit.</para>
/// <para>The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically synchronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients.</para>
/// </remarks>
[AddComponentMenu("Network/NetworkAnimator")]
[RequireComponent(typeof(NetworkIdentity))]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")]
public class NetworkAnimator : NetworkBehaviour
{
[Header("Authority")]
[Tooltip("Set to true if animations come from owner client, set to false if animations always come from server")]
public bool clientAuthority;
/// <summary>
/// The animator component to synchronize.
/// </summary>
[FormerlySerializedAs("m_Animator")]
[Header("Animator")]
[Tooltip("Animator that will have parameters synchronized")]
public Animator animator;
/// <summary>
/// Syncs animator.speed
/// </summary>
[SyncVar(hook = nameof(OnAnimatorSpeedChanged))]
float animatorSpeed;
float previousSpeed;
// Note: not an object[] array because otherwise initialization is real annoying
int[] lastIntParameters;
float[] lastFloatParameters;
bool[] lastBoolParameters;
AnimatorControllerParameter[] parameters;
// multiple layers
int[] animationHash;
int[] transitionHash;
float[] layerWeight;
double nextSendTime;
bool SendMessagesAllowed
{
get
{
if (isServer)
{
if (!clientAuthority)
return true;
// This is a special case where we have client authority but we have not assigned the client who has
// authority over it, no animator data will be sent over the network by the server.
//
// So we check here for a connectionToClient and if it is null we will
// let the server send animation data until we receive an owner.
if (netIdentity != null && netIdentity.connectionToClient == null)
return true;
}
return (hasAuthority && clientAuthority);
}
}
void Awake()
{
// store the animator parameters in a variable - the "Animator.parameters" getter allocates
// a new parameter array every time it is accessed so we should avoid doing it in a loop
parameters = animator.parameters
.Where(par => !animator.IsParameterControlledByCurve(par.nameHash))
.ToArray();
lastIntParameters = new int[parameters.Length];
lastFloatParameters = new float[parameters.Length];
lastBoolParameters = new bool[parameters.Length];
animationHash = new int[animator.layerCount];
transitionHash = new int[animator.layerCount];
layerWeight = new float[animator.layerCount];
}
void FixedUpdate()
{
if (!SendMessagesAllowed)
return;
if (!animator.enabled)
return;
CheckSendRate();
for (int i = 0; i < animator.layerCount; i++)
{
int stateHash;
float normalizedTime;
if (!CheckAnimStateChanged(out stateHash, out normalizedTime, i))
{
continue;
}
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
{
WriteParameters(writer);
SendAnimationMessage(stateHash, normalizedTime, i, layerWeight[i], writer.ToArray());
}
}
CheckSpeed();
}
void CheckSpeed()
{
float newSpeed = animator.speed;
if (Mathf.Abs(previousSpeed - newSpeed) > 0.001f)
{
previousSpeed = newSpeed;
if (isServer)
{
animatorSpeed = newSpeed;
}
else if (isClient)
{
CmdSetAnimatorSpeed(newSpeed);
}
}
}
void OnAnimatorSpeedChanged(float _, float value)
{
// skip if host or client with authority
// they will have already set the speed so don't set again
if (isServer || (hasAuthority && clientAuthority))
return;
animator.speed = value;
}
bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layerId)
{
bool change = false;
stateHash = 0;
normalizedTime = 0;
float lw = animator.GetLayerWeight(layerId);
if (Mathf.Abs(lw - layerWeight[layerId]) > 0.001f)
{
layerWeight[layerId] = lw;
change = true;
}
if (animator.IsInTransition(layerId))
{
AnimatorTransitionInfo tt = animator.GetAnimatorTransitionInfo(layerId);
if (tt.fullPathHash != transitionHash[layerId])
{
// first time in this transition
transitionHash[layerId] = tt.fullPathHash;
animationHash[layerId] = 0;
return true;
}
return change;
}
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(layerId);
if (st.fullPathHash != animationHash[layerId])
{
// first time in this animation state
if (animationHash[layerId] != 0)
{
// came from another animation directly - from Play()
stateHash = st.fullPathHash;
normalizedTime = st.normalizedTime;
}
transitionHash[layerId] = 0;
animationHash[layerId] = st.fullPathHash;
return true;
}
return change;
}
void CheckSendRate()
{
double now = NetworkTime.localTime;
if (SendMessagesAllowed && syncInterval >= 0 && now > nextSendTime)
{
nextSendTime = now + syncInterval;
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
{
if (WriteParameters(writer))
SendAnimationParametersMessage(writer.ToArray());
}
}
}
void SendAnimationMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
{
if (isServer)
{
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
}
else if (isClient)
{
CmdOnAnimationServerMessage(stateHash, normalizedTime, layerId, weight, parameters);
}
}
void SendAnimationParametersMessage(byte[] parameters)
{
if (isServer)
{
RpcOnAnimationParametersClientMessage(parameters);
}
else if (isClient)
{
CmdOnAnimationParametersServerMessage(parameters);
}
}
void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weight, NetworkReader reader)
{
if (hasAuthority && clientAuthority)
return;
// usually transitions will be triggered by parameters, if not, play anims directly.
// NOTE: this plays "animations", not transitions, so any transitions will be skipped.
// NOTE: there is no API to play a transition(?)
if (stateHash != 0 && animator.enabled)
{
animator.Play(stateHash, layerId, normalizedTime);
}
animator.SetLayerWeight(layerId, weight);
ReadParameters(reader);
}
void HandleAnimParamsMsg(NetworkReader reader)
{
if (hasAuthority && clientAuthority)
return;
ReadParameters(reader);
}
void HandleAnimTriggerMsg(int hash)
{
if (animator.enabled)
animator.SetTrigger(hash);
}
void HandleAnimResetTriggerMsg(int hash)
{
if (animator.enabled)
animator.ResetTrigger(hash);
}
ulong NextDirtyBits()
{
ulong dirtyBits = 0;
for (int i = 0; i < parameters.Length; i++)
{
AnimatorControllerParameter par = parameters[i];
bool changed = false;
if (par.type == AnimatorControllerParameterType.Int)
{
int newIntValue = animator.GetInteger(par.nameHash);
changed = newIntValue != lastIntParameters[i];
if (changed)
lastIntParameters[i] = newIntValue;
}
else if (par.type == AnimatorControllerParameterType.Float)
{
float newFloatValue = animator.GetFloat(par.nameHash);
changed = Mathf.Abs(newFloatValue - lastFloatParameters[i]) > 0.001f;
// only set lastValue if it was changed, otherwise value could slowly drift within the 0.001f limit each frame
if (changed)
lastFloatParameters[i] = newFloatValue;
}
else if (par.type == AnimatorControllerParameterType.Bool)
{
bool newBoolValue = animator.GetBool(par.nameHash);
changed = newBoolValue != lastBoolParameters[i];
if (changed)
lastBoolParameters[i] = newBoolValue;
}
if (changed)
{
dirtyBits |= 1ul << i;
}
}
return dirtyBits;
}
bool WriteParameters(NetworkWriter writer, bool forceAll = false)
{
ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits();
writer.WriteULong(dirtyBits);
for (int i = 0; i < parameters.Length; i++)
{
if ((dirtyBits & (1ul << i)) == 0)
continue;
AnimatorControllerParameter par = parameters[i];
if (par.type == AnimatorControllerParameterType.Int)
{
int newIntValue = animator.GetInteger(par.nameHash);
writer.WriteInt(newIntValue);
}
else if (par.type == AnimatorControllerParameterType.Float)
{
float newFloatValue = animator.GetFloat(par.nameHash);
writer.WriteFloat(newFloatValue);
}
else if (par.type == AnimatorControllerParameterType.Bool)
{
bool newBoolValue = animator.GetBool(par.nameHash);
writer.WriteBool(newBoolValue);
}
}
return dirtyBits != 0;
}
void ReadParameters(NetworkReader reader)
{
bool animatorEnabled = animator.enabled;
// need to read values from NetworkReader even if animator is disabled
ulong dirtyBits = reader.ReadULong();
for (int i = 0; i < parameters.Length; i++)
{
if ((dirtyBits & (1ul << i)) == 0)
continue;
AnimatorControllerParameter par = parameters[i];
if (par.type == AnimatorControllerParameterType.Int)
{
int newIntValue = reader.ReadInt();
if (animatorEnabled)
animator.SetInteger(par.nameHash, newIntValue);
}
else if (par.type == AnimatorControllerParameterType.Float)
{
float newFloatValue = reader.ReadFloat();
if (animatorEnabled)
animator.SetFloat(par.nameHash, newFloatValue);
}
else if (par.type == AnimatorControllerParameterType.Bool)
{
bool newBoolValue = reader.ReadBool();
if (animatorEnabled)
animator.SetBool(par.nameHash, newBoolValue);
}
}
}
/// <summary>
/// Custom Serialization
/// </summary>
/// <param name="writer"></param>
/// <param name="initialState"></param>
/// <returns></returns>
public override bool OnSerialize(NetworkWriter writer, bool initialState)
{
bool changed = base.OnSerialize(writer, initialState);
if (initialState)
{
for (int i = 0; i < animator.layerCount; i++)
{
if (animator.IsInTransition(i))
{
AnimatorStateInfo st = animator.GetNextAnimatorStateInfo(i);
writer.WriteInt(st.fullPathHash);
writer.WriteFloat(st.normalizedTime);
}
else
{
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i);
writer.WriteInt(st.fullPathHash);
writer.WriteFloat(st.normalizedTime);
}
writer.WriteFloat(animator.GetLayerWeight(i));
}
WriteParameters(writer, initialState);
return true;
}
return changed;
}
/// <summary>
/// Custom Deserialization
/// </summary>
/// <param name="reader"></param>
/// <param name="initialState"></param>
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
base.OnDeserialize(reader, initialState);
if (initialState)
{
for (int i = 0; i < animator.layerCount; i++)
{
int stateHash = reader.ReadInt();
float normalizedTime = reader.ReadFloat();
animator.SetLayerWeight(i, reader.ReadFloat());
animator.Play(stateHash, i, normalizedTime);
}
ReadParameters(reader);
}
}
/// <summary>
/// Causes an animation trigger to be invoked for a networked object.
/// <para>If local authority is set, and this is called from the client, then the trigger will be invoked on the server and all clients. If not, then this is called on the server, and the trigger will be called on all clients.</para>
/// </summary>
/// <param name="triggerName">Name of trigger.</param>
public void SetTrigger(string triggerName)
{
SetTrigger(Animator.StringToHash(triggerName));
}
/// <summary>
/// Causes an animation trigger to be invoked for a networked object.
/// </summary>
/// <param name="hash">Hash id of trigger (from the Animator).</param>
public void SetTrigger(int hash)
{
if (clientAuthority)
{
if (!isClient)
{
Debug.LogWarning("Tried to set animation in the server for a client-controlled animator");
return;
}
if (!hasAuthority)
{
Debug.LogWarning("Only the client with authority can set animations");
return;
}
if (isClient)
CmdOnAnimationTriggerServerMessage(hash);
// call on client right away
HandleAnimTriggerMsg(hash);
}
else
{
if (!isServer)
{
Debug.LogWarning("Tried to set animation in the client for a server-controlled animator");
return;
}
HandleAnimTriggerMsg(hash);
RpcOnAnimationTriggerClientMessage(hash);
}
}
/// <summary>
/// Causes an animation trigger to be reset for a networked object.
/// <para>If local authority is set, and this is called from the client, then the trigger will be reset on the server and all clients. If not, then this is called on the server, and the trigger will be reset on all clients.</para>
/// </summary>
/// <param name="triggerName">Name of trigger.</param>
public void ResetTrigger(string triggerName)
{
ResetTrigger(Animator.StringToHash(triggerName));
}
/// <summary>
/// Causes an animation trigger to be reset for a networked object.
/// </summary>
/// <param name="hash">Hash id of trigger (from the Animator).</param>
public void ResetTrigger(int hash)
{
if (clientAuthority)
{
if (!isClient)
{
Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator");
return;
}
if (!hasAuthority)
{
Debug.LogWarning("Only the client with authority can reset animations");
return;
}
if (isClient)
CmdOnAnimationResetTriggerServerMessage(hash);
// call on client right away
HandleAnimResetTriggerMsg(hash);
}
else
{
if (!isServer)
{
Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator");
return;
}
HandleAnimResetTriggerMsg(hash);
RpcOnAnimationResetTriggerClientMessage(hash);
}
}
#region server message handlers
[Command]
void CmdOnAnimationServerMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
//Debug.Log($"OnAnimationMessage for netId {netId}");
// handle and broadcast
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
{
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
}
}
[Command]
void CmdOnAnimationParametersServerMessage(byte[] parameters)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// handle and broadcast
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
{
HandleAnimParamsMsg(networkReader);
RpcOnAnimationParametersClientMessage(parameters);
}
}
[Command]
void CmdOnAnimationTriggerServerMessage(int hash)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// handle and broadcast
// host should have already the trigger
bool isHostOwner = isClient && hasAuthority;
if (!isHostOwner)
{
HandleAnimTriggerMsg(hash);
}
RpcOnAnimationTriggerClientMessage(hash);
}
[Command]
void CmdOnAnimationResetTriggerServerMessage(int hash)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// handle and broadcast
// host should have already the trigger
bool isHostOwner = isClient && hasAuthority;
if (!isHostOwner)
{
HandleAnimResetTriggerMsg(hash);
}
RpcOnAnimationResetTriggerClientMessage(hash);
}
[Command]
void CmdSetAnimatorSpeed(float newSpeed)
{
// set animator
animator.speed = newSpeed;
animatorSpeed = newSpeed;
}
#endregion
#region client message handlers
[ClientRpc]
void RpcOnAnimationClientMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
{
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
}
[ClientRpc]
void RpcOnAnimationParametersClientMessage(byte[] parameters)
{
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
HandleAnimParamsMsg(networkReader);
}
[ClientRpc]
void RpcOnAnimationTriggerClientMessage(int hash)
{
// host/owner handles this before it is sent
if (isServer || (clientAuthority && hasAuthority)) return;
HandleAnimTriggerMsg(hash);
}
[ClientRpc]
void RpcOnAnimationResetTriggerClientMessage(int hash)
{
// host/owner handles this before it is sent
if (isServer || (clientAuthority && hasAuthority)) return;
HandleAnimResetTriggerMsg(hash);
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f6f3bf89aa97405989c802ba270f815
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
using System;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// This is a specialized NetworkManager that includes a networked lobby.
/// </summary>
/// <remarks>
/// <para>The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects.</para>
/// <para>NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager.</para>
/// <para>The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called.</para>
/// </remarks>
[AddComponentMenu("Network/NetworkLobbyManager")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
[Obsolete("Use / inherit from NetworkRoomManager instead")]
public class NetworkLobbyManager : NetworkRoomManager {}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4c96e6dd99826849ab1431f94547141
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
using System;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// This component works in conjunction with the NetworkLobbyManager to make up the multiplayer lobby system.
/// <para>The LobbyPrefab object of the NetworkLobbyManager must have this component on it. This component holds basic lobby player data required for the lobby to function. Game specific data for lobby players can be put in other components on the LobbyPrefab or in scripts derived from NetworkLobbyPlayer.</para>
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkLobbyPlayer")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
[Obsolete("Use / inherit from NetworkRoomPlayer instead")]
public class NetworkLobbyPlayer : NetworkRoomPlayer {}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 777a368af85f2e84da7ea5666581921b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
// simple component that holds match information
using System;
using UnityEngine;
namespace Mirror
{
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkMatch")]
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
public class NetworkMatch : NetworkBehaviour
{
///<summary>Set this to the same value on all networked objects that belong to a given match</summary>
public Guid matchId;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5d17e718851449a6879986e45c458fb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// Component that controls visibility of networked objects based on match id.
/// <para>Any object with this component on it will only be visible to other objects in the same match.</para>
/// <para>This would be used to isolate players to their respective matches within a single game server instance. </para>
/// </summary>
// Deprecated 2021-07-16
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkMatchChecker")]
[RequireComponent(typeof(NetworkIdentity))]
[RequireComponent(typeof(NetworkMatch))]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-match-checker")]
public class NetworkMatchChecker : NetworkVisibility
{
// internal for tests
internal static readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchPlayers =
new Dictionary<Guid, HashSet<NetworkIdentity>>();
// internal for tests
internal Guid currentMatch
{
get => GetComponent<NetworkMatch>().matchId;
set => GetComponent<NetworkMatch>().matchId = value;
}
internal Guid lastMatch;
public override void OnStartServer()
{
if (currentMatch == Guid.Empty) return;
if (!matchPlayers.ContainsKey(currentMatch))
matchPlayers.Add(currentMatch, new HashSet<NetworkIdentity>());
matchPlayers[currentMatch].Add(netIdentity);
// No need to rebuild anything here.
// identity.RebuildObservers is called right after this from NetworkServer.SpawnObject
}
public override void OnStopServer()
{
if (currentMatch == Guid.Empty) return;
if (matchPlayers.ContainsKey(currentMatch) && matchPlayers[currentMatch].Remove(netIdentity))
RebuildMatchObservers(currentMatch);
}
void RebuildMatchObservers(Guid specificMatch)
{
foreach (NetworkIdentity networkIdentity in matchPlayers[specificMatch])
networkIdentity?.RebuildObservers(false);
}
#region Observers
/// <summary>
/// Callback used by the visibility system to determine if an observer (player) can see this object.
/// <para>If this function returns true, the network connection will be added as an observer.</para>
/// </summary>
/// <param name="conn">Network connection of a player.</param>
/// <returns>True if the player can see this object.</returns>
public override bool OnCheckObserver(NetworkConnection conn)
{
// Not Visible if not in a match
if (currentMatch == Guid.Empty)
return false;
NetworkMatchChecker networkMatchChecker = conn.identity.GetComponent<NetworkMatchChecker>();
if (networkMatchChecker == null)
return false;
return networkMatchChecker.currentMatch == currentMatch;
}
/// <summary>
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
/// <para>Implementations of this callback should add network connections of players that can see this object to the observers set.</para>
/// </summary>
/// <param name="observers">The new set of observers for this object.</param>
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
{
if (currentMatch == Guid.Empty) return;
foreach (NetworkIdentity networkIdentity in matchPlayers[currentMatch])
if (networkIdentity != null && networkIdentity.connectionToClient != null)
observers.Add(networkIdentity.connectionToClient);
}
#endregion
[ServerCallback]
void Update()
{
// only if changed
if (currentMatch == lastMatch)
return;
// This object is in a new match so observers in the prior match
// and the new match need to rebuild their respective observers lists.
// Remove this object from the hashset of the match it just left
if (lastMatch != Guid.Empty)
{
matchPlayers[lastMatch].Remove(netIdentity);
// RebuildObservers of all NetworkIdentity's in the match this
// object just left
RebuildMatchObservers(lastMatch);
}
if (currentMatch != Guid.Empty)
{
// Make sure this new match is in the dictionary
if (!matchPlayers.ContainsKey(currentMatch))
matchPlayers.Add(currentMatch, new HashSet<NetworkIdentity>());
// Add this object to the hashset of the new match
matchPlayers[currentMatch].Add(netIdentity);
// RebuildObservers of all NetworkIdentity's in the match this object just entered
RebuildMatchObservers(currentMatch);
}
else
{
// Not in any match now...RebuildObservers will clear and add self
netIdentity.RebuildObservers(false);
}
// save last rebuild's match
lastMatch = currentMatch;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1020a74962faada4b807ac5dc053a4cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
using System;
using UnityEngine;
using System.Collections.Generic;
namespace Mirror
{
/// <summary>
/// Component that limits visibility of networked objects to the authority client.
/// <para>Any object with this component on it will only be visible to the client that has been assigned authority for it.</para>
/// <para>This would be used for spawning a non-player networked object for single client to interact with, e.g. in-game puzzles.</para>
/// </summary>
// Deprecated 2021-09-06
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkOwnerChecker")]
[RequireComponent(typeof(NetworkIdentity))]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-owner-checker")]
public class NetworkOwnerChecker : NetworkVisibility
{
/// <summary>
/// Callback used by the visibility system to determine if an observer (player) can see this object.
/// <para>If this function returns true, the network connection will be added as an observer.</para>
/// </summary>
/// <param name="conn">Network connection of a player.</param>
/// <returns>True if the client is the owner of this object.</returns>
public override bool OnCheckObserver(NetworkConnection conn)
{
// Debug.Log($"OnCheckObserver {netIdentity.connectionToClient} {conn}");
return (netIdentity.connectionToClient == conn);
}
/// <summary>
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
/// </summary>
/// <param name="observers">The new set of observers for this object.</param>
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
{
// Do nothing here because the authority client is always added as an observer internally.
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25fd0c51bbe07c140bc30978b91e9182
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
using System;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// Component that will display the clients ping in milliseconds
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkPingDisplay")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-ping-display")]
public class NetworkPingDisplay : MonoBehaviour
{
public Color color = Color.white;
public int padding = 2;
int width = 150;
int height = 25;
void OnGUI()
{
// only while client is active
if (!NetworkClient.active) return;
// show rtt in bottom right corner, right aligned
GUI.color = color;
Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height);
GUIStyle style = GUI.skin.GetStyle("Label");
style.alignment = TextAnchor.MiddleRight;
GUI.Label(rect, $"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms", style);
GUI.color = Color.white;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bc654f29862fc2643b948f772ebb9e68
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// Component that controls visibility of networked objects for players.
/// <para>Any object with this component on it will not be visible to players more than a (configurable) distance away.</para>
/// </summary>
// Deprecated 2021-07-13
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
[AddComponentMenu("Network/NetworkProximityChecker")]
[RequireComponent(typeof(NetworkIdentity))]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-proximity-checker")]
public class NetworkProximityChecker : NetworkVisibility
{
/// <summary>
/// The maximum range that objects will be visible at.
/// </summary>
[Tooltip("The maximum range that objects will be visible at.")]
public int visRange = 10;
/// <summary>
/// How often (in seconds) that this object should update the list of observers that can see it.
/// </summary>
[Tooltip("How often (in seconds) that this object should update the list of observers that can see it.")]
public float visUpdateInterval = 1;
public override void OnStartServer()
{
InvokeRepeating(nameof(RebuildObservers), 0, visUpdateInterval);
}
public override void OnStopServer()
{
CancelInvoke(nameof(RebuildObservers));
}
void RebuildObservers()
{
netIdentity.RebuildObservers(false);
}
/// <summary>
/// Callback used by the visibility system to determine if an observer (player) can see this object.
/// <para>If this function returns true, the network connection will be added as an observer.</para>
/// </summary>
/// <param name="conn">Network connection of a player.</param>
/// <returns>True if the player can see this object.</returns>
public override bool OnCheckObserver(NetworkConnection conn)
{
return Vector3.Distance(conn.identity.transform.position, transform.position) < visRange;
}
/// <summary>
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
/// <para>Implementations of this callback should add network connections of players that can see this object to the observers set.</para>
/// </summary>
/// <param name="observers">The new set of observers for this object.</param>
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
{
// if force hidden then return without adding any observers.
// 'transform.' calls GetComponent, only do it once
Vector3 position = transform.position;
// brute force distance check
// -> only player connections can be observers, so it's enough if we
// go through all connections instead of all spawned identities.
// -> compared to UNET's sphere cast checking, this one is orders of
// magnitude faster. if we have 10k monsters and run a sphere
// cast 10k times, we will see a noticeable lag even with physics
// layers. but checking to every connection is fast.
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
{
if (conn != null && conn.identity != null)
{
// check distance
if (Vector3.Distance(conn.identity.transform.position, position) < visRange)
{
observers.Add(conn);
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1731d8de2d0c84333b08ebe1e79f4118
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,699 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Serialization;
namespace Mirror
{
/// <summary>
/// This is a specialized NetworkManager that includes a networked room.
/// </summary>
/// <remarks>
/// <para>The room has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkRoomPlayer component be on the room player objects.</para>
/// <para>NetworkRoomManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkRoomManager, there are new virtual functions on the NetworkRoomManager that begin with "OnRoom". These should be used on classes derived from NetworkRoomManager instead of the virtual functions on NetworkManager.</para>
/// <para>The OnRoom*() functions have empty implementations on the NetworkRoomManager base class, so the base class functions do not have to be called.</para>
/// </remarks>
[AddComponentMenu("Network/NetworkRoomManager")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
public class NetworkRoomManager : NetworkManager
{
public struct PendingPlayer
{
public NetworkConnection conn;
public GameObject roomPlayer;
}
[Header("Room Settings")]
[FormerlySerializedAs("m_ShowRoomGUI")]
[SerializeField]
[Tooltip("This flag controls whether the default UI is shown for the room")]
public bool showRoomGUI = true;
[FormerlySerializedAs("m_MinPlayers")]
[SerializeField]
[Tooltip("Minimum number of players to auto-start the game")]
public int minPlayers = 1;
[FormerlySerializedAs("m_RoomPlayerPrefab")]
[SerializeField]
[Tooltip("Prefab to use for the Room Player")]
public NetworkRoomPlayer roomPlayerPrefab;
/// <summary>
/// The scene to use for the room. This is similar to the offlineScene of the NetworkManager.
/// </summary>
[Scene]
public string RoomScene;
/// <summary>
/// The scene to use for the playing the game from the room. This is similar to the onlineScene of the NetworkManager.
/// </summary>
[Scene]
public string GameplayScene;
/// <summary>
/// List of players that are in the Room
/// </summary>
[FormerlySerializedAs("m_PendingPlayers")]
public List<PendingPlayer> pendingPlayers = new List<PendingPlayer>();
[Header("Diagnostics")]
/// <summary>
/// True when all players have submitted a Ready message
/// </summary>
[Tooltip("Diagnostic flag indicating all players are ready to play")]
[FormerlySerializedAs("allPlayersReady")]
[SerializeField] bool _allPlayersReady;
/// <summary>
/// These slots track players that enter the room.
/// <para>The slotId on players is global to the game - across all players.</para>
/// </summary>
[Tooltip("List of Room Player objects")]
public List<NetworkRoomPlayer> roomSlots = new List<NetworkRoomPlayer>();
public bool allPlayersReady
{
get => _allPlayersReady;
set
{
bool wasReady = _allPlayersReady;
bool nowReady = value;
if (wasReady != nowReady)
{
_allPlayersReady = value;
if (nowReady)
{
OnRoomServerPlayersReady();
}
else
{
OnRoomServerPlayersNotReady();
}
}
}
}
public override void OnValidate()
{
// always >= 0
maxConnections = Mathf.Max(maxConnections, 0);
// always <= maxConnections
minPlayers = Mathf.Min(minPlayers, maxConnections);
// always >= 0
minPlayers = Mathf.Max(minPlayers, 0);
if (roomPlayerPrefab != null)
{
NetworkIdentity identity = roomPlayerPrefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
roomPlayerPrefab = null;
Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component.");
}
}
base.OnValidate();
}
public void ReadyStatusChanged()
{
int CurrentPlayers = 0;
int ReadyPlayers = 0;
foreach (NetworkRoomPlayer item in roomSlots)
{
if (item != null)
{
CurrentPlayers++;
if (item.readyToBegin)
ReadyPlayers++;
}
}
if (CurrentPlayers == ReadyPlayers)
CheckReadyToBegin();
else
allPlayersReady = false;
}
/// <summary>
/// Called on the server when a client is ready.
/// <para>The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerReady(NetworkConnection conn)
{
Debug.Log("NetworkRoomManager OnServerReady");
base.OnServerReady(conn);
if (conn != null && conn.identity != null)
{
GameObject roomPlayer = conn.identity.gameObject;
// if null or not a room player, don't replace it
if (roomPlayer != null && roomPlayer.GetComponent<NetworkRoomPlayer>() != null)
SceneLoadedForPlayer(conn, roomPlayer);
}
}
void SceneLoadedForPlayer(NetworkConnection conn, GameObject roomPlayer)
{
// Debug.LogFormat(LogType.Log, "NetworkRoom SceneLoadedForPlayer scene: {0} {1}", SceneManager.GetActiveScene().path, conn);
if (IsSceneActive(RoomScene))
{
// cant be ready in room, add to ready list
PendingPlayer pending;
pending.conn = conn;
pending.roomPlayer = roomPlayer;
pendingPlayers.Add(pending);
return;
}
GameObject gamePlayer = OnRoomServerCreateGamePlayer(conn, roomPlayer);
if (gamePlayer == null)
{
// get start position from base class
Transform startPos = GetStartPosition();
gamePlayer = startPos != null
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
: Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
}
if (!OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer))
return;
// replace room player with game player
NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true);
}
/// <summary>
/// CheckReadyToBegin checks all of the players in the room to see if their readyToBegin flag is set.
/// <para>If all of the players are ready, then the server switches from the RoomScene to the PlayScene, essentially starting the game. This is called automatically in response to NetworkRoomPlayer.CmdChangeReadyState.</para>
/// </summary>
public void CheckReadyToBegin()
{
if (!IsSceneActive(RoomScene))
return;
int numberOfReadyPlayers = NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.identity.gameObject.GetComponent<NetworkRoomPlayer>().readyToBegin);
bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers;
if (enoughReadyPlayers)
{
pendingPlayers.Clear();
allPlayersReady = true;
}
else
{
allPlayersReady = false;
}
}
internal void CallOnClientEnterRoom()
{
OnRoomClientEnter();
foreach (NetworkRoomPlayer player in roomSlots)
if (player != null)
{
player.OnClientEnterRoom();
}
}
internal void CallOnClientExitRoom()
{
OnRoomClientExit();
foreach (NetworkRoomPlayer player in roomSlots)
if (player != null)
{
player.OnClientExitRoom();
}
}
#region server handlers
/// <summary>
/// Called on the server when a new client connects.
/// <para>Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerConnect(NetworkConnection conn)
{
if (numPlayers >= maxConnections)
{
conn.Disconnect();
return;
}
// cannot join game in progress
if (!IsSceneActive(RoomScene))
{
conn.Disconnect();
return;
}
base.OnServerConnect(conn);
OnRoomServerConnect(conn);
}
/// <summary>
/// Called on the server when a client disconnects.
/// <para>This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerDisconnect(NetworkConnection conn)
{
if (conn.identity != null)
{
NetworkRoomPlayer roomPlayer = conn.identity.GetComponent<NetworkRoomPlayer>();
if (roomPlayer != null)
roomSlots.Remove(roomPlayer);
foreach (NetworkIdentity clientOwnedObject in conn.clientOwnedObjects)
{
roomPlayer = clientOwnedObject.GetComponent<NetworkRoomPlayer>();
if (roomPlayer != null)
roomSlots.Remove(roomPlayer);
}
}
allPlayersReady = false;
foreach (NetworkRoomPlayer player in roomSlots)
{
if (player != null)
player.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
}
if (IsSceneActive(RoomScene))
RecalculateRoomPlayerIndices();
OnRoomServerDisconnect(conn);
base.OnServerDisconnect(conn);
#if UNITY_SERVER
if (numPlayers < 1)
StopServer();
#endif
}
// Sequential index used in round-robin deployment of players into instances and score positioning
public int clientIndex;
/// <summary>
/// Called on the server when a client adds a new player with NetworkClient.AddPlayer.
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerAddPlayer(NetworkConnection conn)
{
// increment the index before adding the player, so first player starts at 1
clientIndex++;
if (IsSceneActive(RoomScene))
{
if (roomSlots.Count == maxConnections)
return;
allPlayersReady = false;
// Debug.LogFormat(LogType.Log, "NetworkRoomManager.OnServerAddPlayer playerPrefab:{0}", roomPlayerPrefab.name);
GameObject newRoomGameObject = OnRoomServerCreateRoomPlayer(conn);
if (newRoomGameObject == null)
newRoomGameObject = Instantiate(roomPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity);
NetworkServer.AddPlayerForConnection(conn, newRoomGameObject);
}
else
OnRoomServerAddPlayer(conn);
}
[Server]
public void RecalculateRoomPlayerIndices()
{
if (roomSlots.Count > 0)
{
for (int i = 0; i < roomSlots.Count; i++)
{
roomSlots[i].index = i;
}
}
}
/// <summary>
/// This causes the server to switch scenes and sets the networkSceneName.
/// <para>Clients that connect to this server will automatically switch to this scene. This is called automatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene.</para>
/// </summary>
/// <param name="newSceneName"></param>
public override void ServerChangeScene(string newSceneName)
{
if (newSceneName == RoomScene)
{
foreach (NetworkRoomPlayer roomPlayer in roomSlots)
{
if (roomPlayer == null)
continue;
// find the game-player object for this connection, and destroy it
NetworkIdentity identity = roomPlayer.GetComponent<NetworkIdentity>();
if (NetworkServer.active)
{
// re-add the room object
roomPlayer.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject);
}
}
allPlayersReady = false;
}
base.ServerChangeScene(newSceneName);
}
/// <summary>
/// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene().
/// </summary>
/// <param name="sceneName">The name of the new scene.</param>
public override void OnServerSceneChanged(string sceneName)
{
if (sceneName != RoomScene)
{
// call SceneLoadedForPlayer on any players that become ready while we were loading the scene.
foreach (PendingPlayer pending in pendingPlayers)
SceneLoadedForPlayer(pending.conn, pending.roomPlayer);
pendingPlayers.Clear();
}
OnRoomServerSceneChanged(sceneName);
}
/// <summary>
/// This is invoked when a server is started - including when a host is started.
/// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartServer()
{
if (string.IsNullOrEmpty(RoomScene))
{
Debug.LogError("NetworkRoomManager RoomScene is empty. Set the RoomScene in the inspector for the NetworkRoomManager");
return;
}
if (string.IsNullOrEmpty(GameplayScene))
{
Debug.LogError("NetworkRoomManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkRoomManager");
return;
}
OnRoomStartServer();
}
/// <summary>
/// This is invoked when a host is started.
/// <para>StartHost has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartHost()
{
OnRoomStartHost();
}
/// <summary>
/// This is called when a server is stopped - including when a host is stopped.
/// </summary>
public override void OnStopServer()
{
roomSlots.Clear();
OnRoomStopServer();
}
/// <summary>
/// This is called when a host is stopped.
/// </summary>
public override void OnStopHost()
{
OnRoomStopHost();
}
#endregion
#region client handlers
/// <summary>
/// This is invoked when the client is started.
/// </summary>
public override void OnStartClient()
{
if (roomPlayerPrefab == null || roomPlayerPrefab.gameObject == null)
Debug.LogError("NetworkRoomManager no RoomPlayer prefab is registered. Please add a RoomPlayer prefab.");
else
NetworkClient.RegisterPrefab(roomPlayerPrefab.gameObject);
if (playerPrefab == null)
Debug.LogError("NetworkRoomManager no GamePlayer prefab is registered. Please add a GamePlayer prefab.");
OnRoomStartClient();
}
/// <summary>
/// Called on the client when connected to a server.
/// <para>The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects.</para>
/// </summary>
/// <param name="conn">Connection to the server.</param>
public override void OnClientConnect(NetworkConnection conn)
{
OnRoomClientConnect(conn);
base.OnClientConnect(conn);
}
/// <summary>
/// Called on clients when disconnected from a server.
/// <para>This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects.</para>
/// </summary>
/// <param name="conn">Connection to the server.</param>
public override void OnClientDisconnect(NetworkConnection conn)
{
OnRoomClientDisconnect(conn);
base.OnClientDisconnect(conn);
}
/// <summary>
/// This is called when a client is stopped.
/// </summary>
public override void OnStopClient()
{
OnRoomStopClient();
CallOnClientExitRoom();
roomSlots.Clear();
}
/// <summary>
/// Called on clients when a scene has completed loaded, when the scene load was initiated by the server.
/// <para>Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists.</para>
/// </summary>
/// <param name="conn">Connection of the client</param>
public override void OnClientSceneChanged(NetworkConnection conn)
{
if (IsSceneActive(RoomScene))
{
if (NetworkClient.isConnected)
CallOnClientEnterRoom();
}
else
CallOnClientExitRoom();
base.OnClientSceneChanged(conn);
OnRoomClientSceneChanged(conn);
}
#endregion
#region room server virtuals
/// <summary>
/// This is called on the host when a host is started.
/// </summary>
public virtual void OnRoomStartHost() {}
/// <summary>
/// This is called on the host when the host is stopped.
/// </summary>
public virtual void OnRoomStopHost() {}
/// <summary>
/// This is called on the server when the server is started - including when a host is started.
/// </summary>
public virtual void OnRoomStartServer() {}
/// <summary>
/// This is called on the server when the server is started - including when a host is stopped.
/// </summary>
public virtual void OnRoomStopServer() {}
/// <summary>
/// This is called on the server when a new client connects to the server.
/// </summary>
/// <param name="conn">The new connection.</param>
public virtual void OnRoomServerConnect(NetworkConnection conn) {}
/// <summary>
/// This is called on the server when a client disconnects.
/// </summary>
/// <param name="conn">The connection that disconnected.</param>
public virtual void OnRoomServerDisconnect(NetworkConnection conn) {}
/// <summary>
/// This is called on the server when a networked scene finishes loading.
/// </summary>
/// <param name="sceneName">Name of the new scene.</param>
public virtual void OnRoomServerSceneChanged(string sceneName) {}
/// <summary>
/// This allows customization of the creation of the room-player object on the server.
/// <para>By default the roomPlayerPrefab is used to create the room-player, but this function allows that behaviour to be customized.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
/// <returns>The new room-player object.</returns>
public virtual GameObject OnRoomServerCreateRoomPlayer(NetworkConnection conn)
{
return null;
}
/// <summary>
/// This allows customization of the creation of the GamePlayer object on the server.
/// <para>By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the room-player on the connection.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
/// <param name="roomPlayer">The room player object for this connection.</param>
/// <returns>A new GamePlayer object.</returns>
public virtual GameObject OnRoomServerCreateGamePlayer(NetworkConnection conn, GameObject roomPlayer)
{
return null;
}
/// <summary>
/// This allows customization of the creation of the GamePlayer object on the server.
/// <para>This is only called for subsequent GamePlay scenes after the first one.</para>
/// <para>See <see cref="OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)">OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)</see> to customize the player object for the initial GamePlay scene.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
public virtual void OnRoomServerAddPlayer(NetworkConnection conn)
{
base.OnServerAddPlayer(conn);
}
// for users to apply settings from their room player object to their in-game player object
/// <summary>
/// This is called on the server when it is told that a client has finished switching from the room scene to a game player scene.
/// <para>When switching from the room, the room-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the room-player to the game-player object.</para>
/// </summary>
/// <param name="conn">The connection of the player</param>
/// <param name="roomPlayer">The room player object.</param>
/// <param name="gamePlayer">The game player object.</param>
/// <returns>False to not allow this player to replace the room player.</returns>
public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnection conn, GameObject roomPlayer, GameObject gamePlayer)
{
return true;
}
/// <summary>
/// This is called on the server when all the players in the room are ready.
/// <para>The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the room are ready, such as adding a countdown or a confirmation for a group leader.</para>
/// </summary>
public virtual void OnRoomServerPlayersReady()
{
// all players are readyToBegin, start the game
ServerChangeScene(GameplayScene);
}
/// <summary>
/// This is called on the server when CheckReadyToBegin finds that players are not ready
/// <para>May be called multiple times while not ready players are joining</para>
/// </summary>
public virtual void OnRoomServerPlayersNotReady() {}
#endregion
#region room client virtuals
/// <summary>
/// This is a hook to allow custom behaviour when the game client enters the room.
/// </summary>
public virtual void OnRoomClientEnter() {}
/// <summary>
/// This is a hook to allow custom behaviour when the game client exits the room.
/// </summary>
public virtual void OnRoomClientExit() {}
/// <summary>
/// This is called on the client when it connects to server.
/// </summary>
/// <param name="conn">The connection that connected.</param>
public virtual void OnRoomClientConnect(NetworkConnection conn) {}
/// <summary>
/// This is called on the client when disconnected from a server.
/// </summary>
/// <param name="conn">The connection that disconnected.</param>
public virtual void OnRoomClientDisconnect(NetworkConnection conn) {}
/// <summary>
/// This is called on the client when a client is started.
/// </summary>
/// <param name="roomClient">The connection for the room.</param>
public virtual void OnRoomStartClient() {}
/// <summary>
/// This is called on the client when the client stops.
/// </summary>
public virtual void OnRoomStopClient() {}
/// <summary>
/// This is called on the client when the client is finished loading a new networked scene.
/// </summary>
/// <param name="conn">The connection that finished loading a new networked scene.</param>
public virtual void OnRoomClientSceneChanged(NetworkConnection conn) {}
/// <summary>
/// Called on the client when adding a player to the room fails.
/// <para>This could be because the room is full, or the connection is not allowed to have more players.</para>
/// </summary>
public virtual void OnRoomClientAddPlayerFailed() {}
#endregion
#region optional UI
/// <summary>
/// virtual so inheriting classes can roll their own
/// </summary>
public virtual void OnGUI()
{
if (!showRoomGUI)
return;
if (NetworkServer.active && IsSceneActive(GameplayScene))
{
GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f));
if (GUILayout.Button("Return to Room"))
ServerChangeScene(RoomScene);
GUILayout.EndArea();
}
if (IsSceneActive(RoomScene))
GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS");
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 615e6c6589cf9e54cad646b5a11e0529
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,195 @@
using UnityEngine;
namespace Mirror
{
/// <summary>
/// This component works in conjunction with the NetworkRoomManager to make up the multiplayer room system.
/// <para>The RoomPrefab object of the NetworkRoomManager must have this component on it. This component holds basic room player data required for the room to function. Game specific data for room players can be put in other components on the RoomPrefab or in scripts derived from NetworkRoomPlayer.</para>
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkRoomPlayer")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
public class NetworkRoomPlayer : NetworkBehaviour
{
/// <summary>
/// This flag controls whether the default UI is shown for the room player.
/// <para>As this UI is rendered using the old GUI system, it is only recommended for testing purposes.</para>
/// </summary>
[Tooltip("This flag controls whether the default UI is shown for the room player")]
public bool showRoomGUI = true;
[Header("Diagnostics")]
/// <summary>
/// Diagnostic flag indicating whether this player is ready for the game to begin.
/// <para>Invoke CmdChangeReadyState method on the client to set this flag.</para>
/// <para>When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server.</para>
/// </summary>
[Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")]
[SyncVar(hook = nameof(ReadyStateChanged))]
public bool readyToBegin;
/// <summary>
/// Diagnostic index of the player, e.g. Player1, Player2, etc.
/// </summary>
[Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")]
[SyncVar(hook = nameof(IndexChanged))]
public int index;
#region Unity Callbacks
/// <summary>
/// Do not use Start - Override OnStartHost / OnStartClient instead!
/// </summary>
public void Start()
{
if (NetworkManager.singleton is NetworkRoomManager room)
{
// NetworkRoomPlayer object must be set to DontDestroyOnLoad along with NetworkRoomManager
// in server and all clients, otherwise it will be respawned in the game scene which would
// have undesirable effects.
if (room.dontDestroyOnLoad)
DontDestroyOnLoad(gameObject);
room.roomSlots.Add(this);
if (NetworkServer.active)
room.RecalculateRoomPlayerIndices();
if (NetworkClient.active)
room.CallOnClientEnterRoom();
}
else Debug.LogError("RoomPlayer could not find a NetworkRoomManager. The RoomPlayer requires a NetworkRoomManager object to function. Make sure that there is one in the scene.");
}
public virtual void OnDisable()
{
if (NetworkClient.active && NetworkManager.singleton is NetworkRoomManager room)
{
// only need to call this on client as server removes it before object is destroyed
room.roomSlots.Remove(this);
room.CallOnClientExitRoom();
}
}
#endregion
#region Commands
[Command]
public void CmdChangeReadyState(bool readyState)
{
readyToBegin = readyState;
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
if (room != null)
{
room.ReadyStatusChanged();
}
}
#endregion
#region SyncVar Hooks
/// <summary>
/// This is a hook that is invoked on clients when the index changes.
/// </summary>
/// <param name="oldIndex">The old index value</param>
/// <param name="newIndex">The new index value</param>
public virtual void IndexChanged(int oldIndex, int newIndex) {}
/// <summary>
/// This is a hook that is invoked on clients when a RoomPlayer switches between ready or not ready.
/// <para>This function is called when the a client player calls CmdChangeReadyState.</para>
/// </summary>
/// <param name="newReadyState">New Ready State</param>
public virtual void ReadyStateChanged(bool oldReadyState, bool newReadyState) {}
#endregion
#region Room Client Virtuals
/// <summary>
/// This is a hook that is invoked on clients for all room player objects when entering the room.
/// <para>Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called.</para>
/// </summary>
public virtual void OnClientEnterRoom() {}
/// <summary>
/// This is a hook that is invoked on clients for all room player objects when exiting the room.
/// </summary>
public virtual void OnClientExitRoom() {}
#endregion
#region Optional UI
/// <summary>
/// Render a UI for the room. Override to provide your own UI
/// </summary>
public virtual void OnGUI()
{
if (!showRoomGUI)
return;
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
if (room)
{
if (!room.showRoomGUI)
return;
if (!NetworkManager.IsSceneActive(room.RoomScene))
return;
DrawPlayerReadyState();
DrawPlayerReadyButton();
}
}
void DrawPlayerReadyState()
{
GUILayout.BeginArea(new Rect(20f + (index * 100), 200f, 90f, 130f));
GUILayout.Label($"Player [{index + 1}]");
if (readyToBegin)
GUILayout.Label("Ready");
else
GUILayout.Label("Not Ready");
if (((isServer && index > 0) || isServerOnly) && GUILayout.Button("REMOVE"))
{
// This button only shows on the Host for all players other than the Host
// Host and Players can't remove themselves (stop the client instead)
// Host can kick a Player this way.
GetComponent<NetworkIdentity>().connectionToClient.Disconnect();
}
GUILayout.EndArea();
}
void DrawPlayerReadyButton()
{
if (NetworkClient.active && isLocalPlayer)
{
GUILayout.BeginArea(new Rect(20f, 300f, 120f, 20f));
if (readyToBegin)
{
if (GUILayout.Button("Cancel"))
CmdChangeReadyState(false);
}
else
{
if (GUILayout.Button("Ready"))
CmdChangeReadyState(true);
}
GUILayout.EndArea();
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79874ac94d5b1314788ecf0e86bd23fd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Mirror
{
/// <summary>
/// Component that controls visibility of networked objects between scenes.
/// <para>Any object with this component on it will only be visible to other objects in the same scene</para>
/// <para>This would be used when the server has multiple additive subscenes loaded to isolate players to their respective subscenes</para>
/// </summary>
// Deprecated 2021-07-13
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
[DisallowMultipleComponent]
[AddComponentMenu("Network/NetworkSceneChecker")]
[RequireComponent(typeof(NetworkIdentity))]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-scene-checker")]
public class NetworkSceneChecker : NetworkVisibility
{
/// <summary>
/// Flag to force this object to be hidden from all observers.
/// <para>If this object is a player object, it will not be hidden for that client.</para>
/// </summary>
[Tooltip("Enable to force this object to be hidden from all observers.")]
public bool forceHidden;
// Use Scene instead of string scene.name because when additively loading multiples of a subscene the name won't be unique
static readonly Dictionary<Scene, HashSet<NetworkIdentity>> sceneCheckerObjects = new Dictionary<Scene, HashSet<NetworkIdentity>>();
Scene currentScene;
[ServerCallback]
void Awake()
{
currentScene = gameObject.scene;
// Debug.Log($"NetworkSceneChecker.Awake currentScene: {currentScene}");
}
public override void OnStartServer()
{
if (!sceneCheckerObjects.ContainsKey(currentScene))
sceneCheckerObjects.Add(currentScene, new HashSet<NetworkIdentity>());
sceneCheckerObjects[currentScene].Add(netIdentity);
}
public override void OnStopServer()
{
if (sceneCheckerObjects.ContainsKey(currentScene) && sceneCheckerObjects[currentScene].Remove(netIdentity))
RebuildSceneObservers();
}
[ServerCallback]
void Update()
{
if (currentScene == gameObject.scene)
return;
// This object is in a new scene so observers in the prior scene
// and the new scene need to rebuild their respective observers lists.
// Remove this object from the hashset of the scene it just left
sceneCheckerObjects[currentScene].Remove(netIdentity);
// RebuildObservers of all NetworkIdentity's in the scene this object just left
RebuildSceneObservers();
// Set this to the new scene this object just entered
currentScene = gameObject.scene;
// Make sure this new scene is in the dictionary
if (!sceneCheckerObjects.ContainsKey(currentScene))
sceneCheckerObjects.Add(currentScene, new HashSet<NetworkIdentity>());
// Add this object to the hashset of the new scene
sceneCheckerObjects[currentScene].Add(netIdentity);
// RebuildObservers of all NetworkIdentity's in the scene this object just entered
RebuildSceneObservers();
}
void RebuildSceneObservers()
{
foreach (NetworkIdentity networkIdentity in sceneCheckerObjects[currentScene])
if (networkIdentity != null)
networkIdentity.RebuildObservers(false);
}
/// <summary>
/// Callback used by the visibility system to determine if an observer (player) can see this object.
/// <para>If this function returns true, the network connection will be added as an observer.</para>
/// </summary>
/// <param name="conn">Network connection of a player.</param>
/// <returns>True if the player can see this object.</returns>
public override bool OnCheckObserver(NetworkConnection conn)
{
if (forceHidden)
return false;
return conn.identity.gameObject.scene == gameObject.scene;
}
/// <summary>
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
/// <para>Implementations of this callback should add network connections of players that can see this object to the observers set.</para>
/// </summary>
/// <param name="observers">The new set of observers for this object.</param>
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
{
// If forceHidden then return without adding any observers.
if (forceHidden)
return;
// Add everything in the hashset for this object's current scene
foreach (NetworkIdentity networkIdentity in sceneCheckerObjects[currentScene])
if (networkIdentity != null && networkIdentity.connectionToClient != null)
observers.Add(networkIdentity.connectionToClient);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b7fdb599e1359924bad6255660370252
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 44e823b93c7d2477c8796766dc364c59
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
// ʻOumuamua's light curve, assuming little systematic error, presents its
// motion as tumbling, rather than smoothly rotating, and moving sufficiently
// fast relative to the Sun.
//
// A small number of astronomers suggested that ʻOumuamua could be a product of
// alien technology, but evidence in support of this hypothesis is weak.
using UnityEngine;
namespace Mirror
{
[DisallowMultipleComponent]
public class NetworkTransform : NetworkTransformBase
{
protected override Transform targetComponent => transform;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2f74aedd71d9a4f55b3ce499326d45fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,566 @@
// NetworkTransform V2 aka project Oumuamua by vis2k (2021-07)
// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
//
// Base class for NetworkTransform and NetworkTransformChild.
// => simple unreliable sync without any interpolation for now.
// => which means we don't need teleport detection either
//
// NOTE: several functions are virtual in case someone needs to modify a part.
//
// Channel: uses UNRELIABLE at all times.
// -> out of order packets are dropped automatically
// -> it's better than RELIABLE for several reasons:
// * head of line blocking would add delay
// * resending is mostly pointless
// * bigger data race:
// -> if we use a Cmd() at position X over reliable
// -> client gets Cmd() and X at the same time, but buffers X for bufferTime
// -> for unreliable, it would get X before the reliable Cmd(), still
// buffer for bufferTime but end up closer to the original time
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public abstract class NetworkTransformBase : NetworkBehaviour
{
// TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier?
[Header("Authority")]
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority;
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
protected bool IsClientWithAuthority => hasAuthority && clientAuthority;
// target transform to sync. can be on a child.
protected abstract Transform targetComponent { get; }
[Header("Synchronization")]
[Range(0, 1)] public float sendInterval = 0.050f;
public bool syncPosition = true;
public bool syncRotation = true;
// scale sync is rare. off by default.
public bool syncScale = false;
double lastClientSendTime;
double lastServerSendTime;
// not all games need to interpolate. a board game might jump to the
// final position immediately.
[Header("Interpolation")]
public bool interpolatePosition = true;
public bool interpolateRotation = true;
public bool interpolateScale = false;
// "Experimentally Ive found that the amount of delay that works best
// at 2-5% packet loss is 3X the packet send rate"
// NOTE: we do NOT use a dyanmically changing buffer size.
// it would come with a lot of complications, e.g. buffer time
// advantages/disadvantages for different connections.
// Glenn Fiedler's recommendation seems solid, and should cover
// the vast majority of connections.
// (a player with 2000ms latency will have issues no matter what)
[Header("Buffering")]
[Tooltip("Snapshots are buffered for sendInterval * multiplier seconds. If your expected client base is to run at non-ideal connection quality (2-5% packet loss), 3x supposedly works best.")]
public int bufferTimeMultiplier = 1;
public float bufferTime => sendInterval * bufferTimeMultiplier;
[Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")]
public int bufferSizeLimit = 64;
[Tooltip("Start to accelerate interpolation if buffer size is >= threshold. Needs to be larger than bufferTimeMultiplier.")]
public int catchupThreshold = 4;
[Tooltip("Once buffer is larger catchupThreshold, accelerate by multiplier % per excess entry.")]
[Range(0, 1)] public float catchupMultiplier = 0.10f;
// snapshots sorted by timestamp
// in the original article, glenn fiedler drops any snapshots older than
// the last received snapshot.
// -> instead, we insert into a sorted buffer
// -> the higher the buffer information density, the better
// -> we still drop anything older than the first element in the buffer
// => internal for testing
//
// IMPORTANT: of explicit 'NTSnapshot' type instead of 'Snapshot'
// interface because List<interface> allocates through boxing
internal SortedList<double, NTSnapshot> serverBuffer = new SortedList<double, NTSnapshot>();
internal SortedList<double, NTSnapshot> clientBuffer = new SortedList<double, NTSnapshot>();
// absolute interpolation time, moved along with deltaTime
// (roughly between [0, delta] where delta is snapshot B - A timestamp)
// (can be bigger than delta when overshooting)
double serverInterpolationTime;
double clientInterpolationTime;
// only convert the static Interpolation function to Func<T> once to
// avoid allocations
Func<NTSnapshot, NTSnapshot, double, NTSnapshot> Interpolate = NTSnapshot.Interpolate;
[Header("Debug")]
public bool showGizmos;
public bool showOverlay;
public Color overlayColor = new Color(0, 0, 0, 0.5f);
// snapshot functions //////////////////////////////////////////////////
// construct a snapshot of the current state
// => internal for testing
protected virtual NTSnapshot ConstructSnapshot()
{
// NetworkTime.localTime for double precision until Unity has it too
return new NTSnapshot(
// our local time is what the other end uses as remote time
NetworkTime.localTime,
// the other end fills out local time itself
0,
targetComponent.localPosition,
targetComponent.localRotation,
targetComponent.localScale
);
}
// apply a snapshot to the Transform.
// -> start, end, interpolated are all passed in caes they are needed
// -> a regular game would apply the 'interpolated' snapshot
// -> a board game might want to jump to 'goal' directly
// (it's easier to always interpolate and then apply selectively,
// instead of manually interpolating x, y, z, ... depending on flags)
// => internal for testing
//
// NOTE: stuck detection is unnecessary here.
// we always set transform.position anyway, we can't get stuck.
protected virtual void ApplySnapshot(NTSnapshot start, NTSnapshot goal, NTSnapshot interpolated)
{
// local position/rotation for VR support
//
// if syncPosition/Rotation/Scale is disabled then we received nulls
// -> current position/rotation/scale would've been added as snapshot
// -> we still interpolated
// -> but simply don't apply it. if the user doesn't want to sync
// scale, then we should not touch scale etc.
if (syncPosition)
targetComponent.localPosition = interpolatePosition ? interpolated.position : goal.position;
if (syncRotation)
targetComponent.localRotation = interpolateRotation ? interpolated.rotation : goal.rotation;
if (syncScale)
targetComponent.localScale = interpolateScale ? interpolated.scale : goal.scale;
}
// cmd /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
OnClientToServerSync(position, rotation, scale);
//For client authority, immediately pass on the client snapshot to all other
//clients instead of waiting for server to send its snapshots.
if (clientAuthority)
{
RpcServerToClientSync(position, rotation, scale);
}
}
// local authority client sends sync message to server for broadcasting
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// only apply if in client authority mode
if (!clientAuthority) return;
// protect against ever growing buffer size attacks
if (serverBuffer.Count >= bufferSizeLimit) return;
// only player owned objects (with a connection) can send to
// server. we can get the timestamp from the connection.
double timestamp = connectionToClient.remoteTimeStamp;
// position, rotation, scale can have no value if same as last time.
// saves bandwidth.
// but we still need to feed it to snapshot interpolation. we can't
// just have gaps in there if nothing has changed. for example, if
// client sends snapshot at t=0
// client sends nothing for 10s because not moved
// client sends snapshot at t=10
// then the server would assume that it's one super slow move and
// replay it for 10 seconds.
if (!position.HasValue) position = targetComponent.localPosition;
if (!rotation.HasValue) rotation = targetComponent.localRotation;
if (!scale.HasValue) scale = targetComponent.localScale;
// construct snapshot with batch timestamp to save bandwidth
NTSnapshot snapshot = new NTSnapshot(
timestamp,
NetworkTime.localTime,
position.Value, rotation.Value, scale.Value
);
// add to buffer (or drop if older than first element)
SnapshotInterpolation.InsertIfNewEnough(snapshot, serverBuffer);
}
// rpc /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
OnServerToClientSync(position, rotation, scale);
// server broadcasts sync message to all clients
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// in host mode, the server sends rpcs to all clients.
// the host client itself will receive them too.
// -> host server is always the source of truth
// -> we can ignore any rpc on the host client
// => otherwise host objects would have ever growing clientBuffers
// (rpc goes to clients. if isServer is true too then we are host)
if (isServer) return;
// don't apply for local player with authority
if (IsClientWithAuthority) return;
// protect against ever growing buffer size attacks
if (clientBuffer.Count >= bufferSizeLimit) return;
// on the client, we receive rpcs for all entities.
// not all of them have a connectionToServer.
// but all of them go through NetworkClient.connection.
// we can get the timestamp from there.
double timestamp = NetworkClient.connection.remoteTimeStamp;
// position, rotation, scale can have no value if same as last time.
// saves bandwidth.
// but we still need to feed it to snapshot interpolation. we can't
// just have gaps in there if nothing has changed. for example, if
// client sends snapshot at t=0
// client sends nothing for 10s because not moved
// client sends snapshot at t=10
// then the server would assume that it's one super slow move and
// replay it for 10 seconds.
if (!position.HasValue) position = targetComponent.localPosition;
if (!rotation.HasValue) rotation = targetComponent.localRotation;
if (!scale.HasValue) scale = targetComponent.localScale;
// construct snapshot with batch timestamp to save bandwidth
NTSnapshot snapshot = new NTSnapshot(
timestamp,
NetworkTime.localTime,
position.Value, rotation.Value, scale.Value
);
// add to buffer (or drop if older than first element)
SnapshotInterpolation.InsertIfNewEnough(snapshot, clientBuffer);
}
// update //////////////////////////////////////////////////////////////
void UpdateServer()
{
// broadcast to all clients each 'sendInterval'
// (client with authority will drop the rpc)
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
//
// Checks to ensure server only sends snapshots if object is
// on server authority(!clientAuthority) mode because on client
// authority mode snapshots are broadcasted right after the authoritative
// client updates server in the command function(see above), OR,
// since host does not send anything to update the server, any client
// authoritative movement done by the host will have to be broadcasted
// here by checking IsClientWithAuthority.
if (NetworkTime.localTime >= lastServerSendTime + sendInterval &&
(!clientAuthority || IsClientWithAuthority))
{
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
NTSnapshot snapshot = ConstructSnapshot();
RpcServerToClientSync(
// only sync what the user wants to sync
syncPosition ? snapshot.position : new Vector3?(),
syncRotation? snapshot.rotation : new Quaternion?(),
syncScale ? snapshot.scale : new Vector3?()
);
lastServerSendTime = NetworkTime.localTime;
}
// apply buffered snapshots IF client authority
// -> in server authority, server moves the object
// so no need to apply any snapshots there.
// -> don't apply for host mode player objects either, even if in
// client authority mode. if it doesn't go over the network,
// then we don't need to do anything.
if (clientAuthority && !hasAuthority)
{
// compute snapshot interpolation & apply if any was spit out
// TODO we don't have Time.deltaTime double yet. float is fine.
if (SnapshotInterpolation.Compute(
NetworkTime.localTime, Time.deltaTime,
ref serverInterpolationTime,
bufferTime, serverBuffer,
catchupThreshold, catchupMultiplier,
Interpolate,
out NTSnapshot computed))
{
NTSnapshot start = serverBuffer.Values[0];
NTSnapshot goal = serverBuffer.Values[1];
ApplySnapshot(start, goal, computed);
}
}
}
void UpdateClient()
{
// client authority, and local player (= allowed to move myself)?
if (IsClientWithAuthority)
{
// send to server each 'sendInterval'
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
if (NetworkTime.localTime >= lastClientSendTime + sendInterval)
{
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
NTSnapshot snapshot = ConstructSnapshot();
CmdClientToServerSync(
// only sync what the user wants to sync
syncPosition ? snapshot.position : new Vector3?(),
syncRotation? snapshot.rotation : new Quaternion?(),
syncScale ? snapshot.scale : new Vector3?()
);
lastClientSendTime = NetworkTime.localTime;
}
}
// for all other clients (and for local player if !authority),
// we need to apply snapshots from the buffer
else
{
// compute snapshot interpolation & apply if any was spit out
// TODO we don't have Time.deltaTime double yet. float is fine.
if (SnapshotInterpolation.Compute(
NetworkTime.localTime, Time.deltaTime,
ref clientInterpolationTime,
bufferTime, clientBuffer,
catchupThreshold, catchupMultiplier,
Interpolate,
out NTSnapshot computed))
{
NTSnapshot start = clientBuffer.Values[0];
NTSnapshot goal = clientBuffer.Values[1];
ApplySnapshot(start, goal, computed);
}
}
}
void Update()
{
// if server then always sync to others.
if (isServer) UpdateServer();
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
else if (isClient) UpdateClient();
}
// common Teleport code for client->server and server->client
protected virtual void OnTeleport(Vector3 destination)
{
// reset any in-progress interpolation & buffers
Reset();
// set the new position.
// interpolation will automatically continue.
targetComponent.position = destination;
// TODO
// what if we still receive a snapshot from before the interpolation?
// it could easily happen over unreliable.
// -> maybe add destionation as first entry?
}
// server->client teleport to force position without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[ClientRpc]
public void RpcTeleport(Vector3 destination)
{
// NOTE: even in client authority mode, the server is always allowed
// to teleport the player. for example:
// * CmdEnterPortal() might teleport the player
// * Some people use client authority with server sided checks
// so the server should be able to reset position if needed.
// TODO what about host mode?
OnTeleport(destination);
}
// client->server teleport to force position without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[Command]
public void CmdTeleport(Vector3 destination)
{
// client can only teleport objects that it has authority over.
if (!clientAuthority) return;
// TODO what about host mode?
OnTeleport(destination);
// if a client teleports, we need to broadcast to everyone else too
// TODO the teleported client should ignore the rpc though.
// otherwise if it already moved again after teleporting,
// the rpc would come a little bit later and reset it once.
// TODO or not? if client ONLY calls Teleport(pos), the position
// would only be set after the rpc. unless the client calls
// BOTH Teleport(pos) and targetComponent.position=pos
RpcTeleport(destination);
}
protected virtual void Reset()
{
// disabled objects aren't updated anymore.
// so let's clear the buffers.
serverBuffer.Clear();
clientBuffer.Clear();
// reset interpolation time too so we start at t=0 next time
serverInterpolationTime = 0;
clientInterpolationTime = 0;
}
protected virtual void OnDisable() => Reset();
protected virtual void OnEnable() => Reset();
protected virtual void OnValidate()
{
// make sure that catchup threshold is > buffer multiplier.
// for a buffer multiplier of '3', we usually have at _least_ 3
// buffered snapshots. often 4-5 even.
//
// catchUpThreshold should be a minimum of bufferTimeMultiplier + 3,
// to prevent clashes with SnapshotInterpolation looking for at least
// 3 old enough buffers, else catch up will be implemented while there
// is not enough old buffers, and will result in jitter.
// (validated with several real world tests by ninja & imer)
catchupThreshold = Mathf.Max(bufferTimeMultiplier + 3, catchupThreshold);
// buffer limit should be at least multiplier to have enough in there
bufferSizeLimit = Mathf.Max(bufferTimeMultiplier, bufferSizeLimit);
}
// OnGUI allocates even if it does nothing. avoid in release.
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// debug ///////////////////////////////////////////////////////////////
protected virtual void OnGUI()
{
if (!showOverlay) return;
// show data next to player for easier debugging. this is very useful!
// IMPORTANT: this is basically an ESP hack for shooter games.
// DO NOT make this available with a hotkey in release builds
if (!Debug.isDebugBuild) return;
// project position to screen
Vector3 point = Camera.main.WorldToScreenPoint(targetComponent.position);
// enough alpha, in front of camera and in screen?
if (point.z >= 0 && Utils.IsPointInScreen(point))
{
// catchup is useful to show too
int serverBufferExcess = Mathf.Max(serverBuffer.Count - catchupThreshold, 0);
int clientBufferExcess = Mathf.Max(clientBuffer.Count - catchupThreshold, 0);
float serverCatchup = serverBufferExcess * catchupMultiplier;
float clientCatchup = clientBufferExcess * catchupMultiplier;
GUI.color = overlayColor;
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
// always show both client & server buffers so it's super
// obvious if we accidentally populate both.
GUILayout.Label($"Server Buffer:{serverBuffer.Count}");
if (serverCatchup > 0)
GUILayout.Label($"Server Catchup:{serverCatchup*100:F2}%");
GUILayout.Label($"Client Buffer:{clientBuffer.Count}");
if (clientCatchup > 0)
GUILayout.Label($"Client Catchup:{clientCatchup*100:F2}%");
GUILayout.EndArea();
GUI.color = Color.white;
}
}
protected virtual void DrawGizmos(SortedList<double, NTSnapshot> buffer)
{
// only draw if we have at least two entries
if (buffer.Count < 2) return;
// calcluate threshold for 'old enough' snapshots
double threshold = NetworkTime.localTime - bufferTime;
Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
// draw the whole buffer for easier debugging.
// it's worth seeing how much we have buffered ahead already
for (int i = 0; i < buffer.Count; ++i)
{
// color depends on if old enough or not
NTSnapshot entry = buffer.Values[i];
bool oldEnough = entry.localTimestamp <= threshold;
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
Gizmos.DrawCube(entry.position, Vector3.one);
}
// extra: lines between start<->position<->goal
Gizmos.color = Color.green;
Gizmos.DrawLine(buffer.Values[0].position, targetComponent.position);
Gizmos.color = Color.white;
Gizmos.DrawLine(targetComponent.position, buffer.Values[1].position);
}
protected virtual void OnDrawGizmos()
{
// This fires in edit mode but that spams NRE's so check isPlaying
if (!Application.isPlaying) return;
if (!showGizmos) return;
if (isServer) DrawGizmos(serverBuffer);
if (isClient) DrawGizmos(clientBuffer);
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e77294d8ccbc4e7cb8ca2bd0d3e99ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
// A component to synchronize the position of child transforms of networked objects.
// There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the recieved values.
using UnityEngine;
namespace Mirror
{
public class NetworkTransformChild : NetworkTransformBase
{
[Header("Target")]
public Transform target;
protected override Transform targetComponent => target;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 734b48bea0b204338958ee3d885e11f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,62 @@
// snapshot for snapshot interpolation
// https://gafferongames.com/post/snapshot_interpolation/
// position, rotation, scale for compatibility for now.
using UnityEngine;
namespace Mirror
{
// NetworkTransform Snapshot
public struct NTSnapshot : Snapshot
{
// time or sequence are needed to throw away older snapshots.
//
// glenn fiedler starts with a 16 bit sequence number.
// supposedly this is meant as a simplified example.
// in the end we need the remote timestamp for accurate interpolation
// and buffering over time.
//
// note: in theory, IF server sends exactly(!) at the same interval then
// the 16 bit ushort timestamp would be enough to calculate the
// remote time (sequence * sendInterval). but Unity's update is
// not guaranteed to run on the exact intervals / do catchup.
// => remote timestamp is better for now
//
// [REMOTE TIME, NOT LOCAL TIME]
// => DOUBLE for long term accuracy & batching gives us double anyway
public double remoteTimestamp { get; set; }
public double localTimestamp { get; set; }
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public NTSnapshot(double remoteTimestamp, double localTimestamp, Vector3 position, Quaternion rotation, Vector3 scale)
{
this.remoteTimestamp = remoteTimestamp;
this.localTimestamp = localTimestamp;
this.position = position;
this.rotation = rotation;
this.scale = scale;
}
public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t)
{
// NOTE:
// Vector3 & Quaternion components are float anyway, so we can
// keep using the functions with 't' as float instead of double.
return new NTSnapshot(
// interpolated snapshot is applied directly. don't need timestamps.
0, 0,
// lerp position/rotation/scale unclamped in case we ever need
// to extrapolate. atm SnapshotInterpolation never does.
Vector3.LerpUnclamped(from.position, to.position, (float)t),
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
// (0, 90, 1.5) is even worse. for Lerp.
// => Slerp works way better for our euler angles.
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3dae77b43dc4e1dbb2012924b2da79c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2539267b6934a4026a505690a1e1eda2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
using System.IO;
using UnityEditor;
using UnityEngine;
namespace Mirror
{
public static class EditorHelper
{
public static string FindPath<T>()
{
string typeName = typeof(T).Name;
string[] guidsFound = AssetDatabase.FindAssets($"t:Script {typeName}");
if (guidsFound.Length >= 1 && !string.IsNullOrEmpty(guidsFound[0]))
{
if (guidsFound.Length > 1)
{
Debug.LogWarning($"Found more than one{typeName}");
}
string path = AssetDatabase.GUIDToAssetPath(guidsFound[0]);
return Path.GetDirectoryName(path);
}
else
{
Debug.LogError($"Could not find path of {typeName}");
return string.Empty;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dba787f167ff29c4288532af1ec3584c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 62c8dc5bb12bbc6428bb66ccbac57000
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1 @@
// File moved to Mirror/Editor/Logging/LogLevelWindow.cs

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f28def2148ed5194abe70af012a4e3e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More