浏览代码

Merging in latest from Staging

/main/staging/Open_beta_Update/open_beta_relay
当前提交
1a5dccac
共有 21 个文件被更改,包括 504 次插入408 次删除
  1. 94
      Assets/Prefabs/UI/JoinContent.prefab
  2. 86
      Assets/Prefabs/UI/JoinCreateCanvas.prefab
  3. 156
      Assets/Scenes/mainScene.unity
  4. 70
      Assets/Scripts/Game/GameManager.cs
  5. 1
      Assets/Scripts/Game/LobbyServiceData.cs
  6. 9
      Assets/Scripts/Game/LocalGameState.cs
  7. 1
      Assets/Scripts/Game/LocalLobby.cs
  8. 4
      Assets/Scripts/Infrastructure/LogHandler.cs
  9. 128
      Assets/Scripts/Infrastructure/UpdateSlow.cs
  10. 128
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  11. 2
      Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
  12. 12
      Assets/Scripts/Relay/RelayAPIInterface.cs
  13. 22
      Assets/Scripts/Relay/RelayUtpClient.cs
  14. 4
      Assets/Scripts/Relay/RelayUtpHost.cs
  15. 16
      Assets/Scripts/Relay/RelayUtpSetup.cs
  16. 108
      Assets/Scripts/Tests/PlayMode/UpdateSlowTests.cs
  17. 2
      Assets/Scripts/UI/JoinMenuUI.cs
  18. 12
      Assets/Scripts/UI/SpinnerUI.cs
  19. 11
      Assets/Scripts/UI/UIPanelBase.cs
  20. 35
      Assets/Scripts/UI/RateLimitVisibility.cs
  21. 11
      Assets/Scripts/UI/RateLimitVisibility.cs.meta

94
Assets/Prefabs/UI/JoinContent.prefab


- component: {fileID: 7588080595629456861}
- component: {fileID: 6677898783497457153}
- component: {fileID: 2237434810424445170}
- component: {fileID: 3932890662139234867}
- component: {fileID: 3262820153109060702}
- component: {fileID: 1874295389668054338}
m_Layer: 5
m_Name: RefreshButton
m_TagString: Untagged

m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!225 &3932890662139234867
CanvasGroup:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4272638571207087159}
m_Enabled: 1
m_Alpha: 1
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!114 &3262820153109060702
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4272638571207087159}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ab355d5994635724dbde297a055fb586, type: 3}
m_Name:
m_EditorClassIdentifier:
m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
--- !u!114 &1874295389668054338
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4272638571207087159}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 771ddda4ce2ee2a4dad57866ecc1170d, type: 3}
m_Name:
m_EditorClassIdentifier:
m_target: {fileID: 3262820153109060702}
m_alphaWhenHidden: 0.5
m_requestType: 0
--- !u!1 &4581814564158191570
GameObject:
m_ObjectHideFlags: 0

- component: {fileID: 4781496634016831221}
- component: {fileID: 433211913614645534}
- component: {fileID: 2049737766857408132}
- component: {fileID: 3395876640994339088}
- component: {fileID: 2856360851590408532}
- component: {fileID: 4536627014072050652}
m_Layer: 5
m_Name: JoinButton
m_TagString: Untagged

m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!225 &3395876640994339088
CanvasGroup:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8940655666626669338}
m_Enabled: 1
m_Alpha: 1
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!114 &2856360851590408532
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8940655666626669338}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ab355d5994635724dbde297a055fb586, type: 3}
m_Name:
m_EditorClassIdentifier:
m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
--- !u!114 &4536627014072050652
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8940655666626669338}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 771ddda4ce2ee2a4dad57866ecc1170d, type: 3}
m_Name:
m_EditorClassIdentifier:
m_target: {fileID: 2856360851590408532}
m_alphaWhenHidden: 0.5
m_requestType: 1
--- !u!1 &9182743073287606781
GameObject:
m_ObjectHideFlags: 0

m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1462126939442648229}
m_TargetAssemblyTypeName: LobbyRooms.UI.JoinMenuUI, LobbyRooms
m_MethodName: OnJoinCodeInputFieldChanged
m_TargetAssemblyTypeName: LobbyRelaySample.UI.JoinMenuUI, LobbyRelaySample
m_MethodName: OnLobbyCodeInputFieldChanged
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}

86
Assets/Prefabs/UI/JoinCreateCanvas.prefab


m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 30.2
m_fontSize: 22.8
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 1

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 30.2
m_fontSize: 22.8
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 1

m_Modification:
m_TransformParent: {fileID: 1119140321553661053}
m_Modifications:
- target: {fileID: 1079176168591545865, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1079176168591545865, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Target
value:
objectReference: {fileID: 4578721078997909056}
- target: {fileID: 1079176168591545865, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_CallState
value: 2
objectReference: {fileID: 0}
- target: {fileID: 1079176168591545865, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName
value: Hide
objectReference: {fileID: 0}
- target: {fileID: 1079176168591545865, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_TargetAssemblyTypeName
value: LobbyRooms.UI.UIPanelBase, LobbyRooms
objectReference: {fileID: 0}
- target: {fileID: 1079176168591545865, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_ObjectArgumentAssemblyTypeName
value: UnityEngine.Object, UnityEngine
objectReference: {fileID: 0}
objectReference: {fileID: 0}
- target: {fileID: 3939988936382120653, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_AnchoredPosition.y
value: -0.5
objectReference: {fileID: 0}
- target: {fileID: 4388255649222666380, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5860664165358648575, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_VerticalAlignment
value: 512
objectReference: {fileID: 0}
- target: {fileID: 5860664165358648575, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_HorizontalAlignment
value: 2
- target: {fileID: 5912780227213918576, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_fontSize
value: 18
objectReference: {fileID: 0}
- target: {fileID: 5962604622760643426, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8270614565048916457, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_Alpha
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8270614565048916457, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_Interactable
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8533460643281418118, guid: 328b912adedf1bc41a44f60a12723cc0, type: 3}
propertyPath: m_AnchorMax.y
value: 0

m_Modification:
m_TransformParent: {fileID: 1119140321553661053}
m_Modifications:
- target: {fileID: 433211913614645534, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 433211913614645534, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode
value: 1

propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7522528203600751071, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_Size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7573825319354851387, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchorMax.x
value: 0

value: 0
objectReference: {fileID: 0}
- target: {fileID: 8242294458145102565, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 9032799187230319547, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_Size
value: 0.99999994
objectReference: {fileID: 0}
- target: {fileID: 9095321446247494771, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 9095321446247494771, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 9095321446247494771, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 9095321446247494771, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 9095321446247494771, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 9095321446247494771, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}

156
Assets/Scenes/mainScene.unity


m_Name:
m_EditorClassIdentifier:
m_editorLogVerbosity: 0
m_popUpPrefab: {fileID: 2974111728825125460, guid: 79d6084439b78bb4eaf5232cb953fd87, type: 3}
m_popUpPrefab: {fileID: 2974111728825125460, guid: 79d6084439b78bb4eaf5232cb953fd87,
type: 3}
m_reaction:
m_logMessageCallback:
m_PersistentCalls:

- target: {fileID: 273761196813220610, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_IntArgument
value: 3
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 326167899787007624, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_fontColor32.rgba
value: 4291809231
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2545639037669962845, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 2637199316291327119, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: -12.5
value: -12.500015
objectReference: {fileID: 0}
- target: {fileID: 2637199316850714327, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_PresetInfoIsWorld

propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2637199316974547451, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2637199317172016808, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_IsActive

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2918310285625255774, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2995192838028648417, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 4822032080772604407, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
value: -0.00006490946
value: -0.000015258789
value: -0.2306938
objectReference: {fileID: 0}
- target: {fileID: 4824240073023402834, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size
value: 2
objectReference: {fileID: 0}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode
value: 6
objectReference: {fileID: 0}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Target
value:
objectReference: {fileID: 2637199315837045700}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_CallState
value: 2
objectReference: {fileID: 0}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName
value: set_isOn
objectReference: {fileID: 0}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_TargetAssemblyTypeName
value: UnityEngine.UI.Toggle, UnityEngine.UI
objectReference: {fileID: 0}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_BoolArgument
value: 1
objectReference: {fileID: 0}
- target: {fileID: 5016612605613956886, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_ObjectArgumentAssemblyTypeName
value: UnityEngine.Object, UnityEngine
objectReference: {fileID: 0}
- target: {fileID: 5044563295281636569, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

value: 0
objectReference: {fileID: 0}
- target: {fileID: 5164090239530900493, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}

value: 0
objectReference: {fileID: 0}
- target: {fileID: 6970381027661675607, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}

m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045700 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 7695022432396603176, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_PrefabInstance: {fileID: 2637199315837045693}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1001 &7716713812904700119

70
Assets/Scripts/Game/GameManager.cs


using System;
using LobbyRelaySample.relay;
using System.Collections;
using System.Collections.Generic;

private LobbyContentHeartbeat m_lobbyContentHeartbeat = new LobbyContentHeartbeat();
private RelayUtpSetup m_relaySetup;
private RelayUtpClient m_relayClient;
// The Lobby API rate limits query requests to one every 1.5s, and it will return a 429 "Too Many Requests" error otherwise.
private const float k_lobbyAssignmentCoolingSeconds = 1.5f;
private bool m_coolingDown;
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
public void SetLobbyColorFilter(int color)

public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.RenameRequest)
{
m_localUser.DisplayName = (string)msg;
{ m_localUser.DisplayName = (string)msg;
if (!LobbyRequestCooledDown())
return;
{
lobby.ToLocalLobby.Convert(r, m_localLobby);
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnCreatedLobby();
},
OnFailedJoin);

if (!LobbyRequestCooledDown())
return;
{
lobby.ToLocalLobby.Convert(r, m_localLobby);
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnJoinedLobby();
},
OnFailedJoin);

if (!LobbyRequestCooledDown())
return;
qr =>
{
qr => {
er =>
{
long errorLong = 0;
OnLobbyQueryFailed(errorLong); // TODO: What to supply here?
er => {
OnLobbyQueryFailed();
{
SetGameState((GameState)msg);
{ SetGameState((GameState)msg);
{
EmoteType emote = (EmoteType)msg;
{ EmoteType emote = (EmoteType)msg;
{
m_localUser.UserStatus = (UserStatus)msg;
{ m_localUser.UserStatus = (UserStatus)msg;
{
BeginCountDown();
{ BeginCountDown();
{
m_localLobby.State = LobbyState.Lobby;
{ m_localLobby.State = LobbyState.Lobby;
{
m_localUser.UserStatus = UserStatus.InGame;
{ m_localUser.UserStatus = UserStatus.InGame;
{
m_localLobby.State = LobbyState.Lobby;
{ m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
SetUserLobbyState();
}

m_lobbyServiceData.CurrentLobbies = newLobbyDict;
}
private void OnLobbyQueryFailed(long errorCode)
private void OnLobbyQueryFailed()
m_lobbyServiceData.lastErrorCode = errorCode;
m_lobbyServiceData.State = LobbyQueryState.Error;
}

Component.Destroy(m_relayClient);
m_relayClient = null;
}
}
private bool LobbyRequestCooledDown()
{
if (m_coolingDown)
return false;
StartCoroutine(RequestCoolDown());
return true;
}
private IEnumerator RequestCoolDown()
{
m_coolingDown = true;
yield return new WaitForSeconds(k_lobbyAssignmentCoolingSeconds);
m_coolingDown = false;
}
/// <summary>

1
Assets/Scripts/Game/LobbyServiceData.cs


{
LobbyQueryState m_CurrentState = LobbyQueryState.Empty;
public long lastErrorCode;
public LobbyQueryState State
{
get { return m_CurrentState; }

9
Assets/Scripts/Game/LocalGameState.cs


get => m_State;
set
{
m_State = value;
OnChanged(this);
if (m_State != value)
{
m_State = value;
OnChanged(this);
}
if (m_State == oldObserved.State)
return;
m_State = oldObserved.State;
OnChanged(this);
}

1
Assets/Scripts/Game/LocalLobby.cs


ServerAddress m_relayServer;
/// <summary>Used only for visual output of the Relay connection info. The obfuscated Relay server IP is obtained during allocation in the RelayUtpSetup.</summary>
public ServerAddress RelayServer
{
get => m_relayServer;

4
Assets/Scripts/Infrastructure/LogHandler.cs


}
/// <summary>
/// The idea here is to
/// The idea here is to present the most relevant error first.
/// </summary>
[Serializable]
public class ErrorReaction

string message = "";
var rawExceptionMessage = "";
//We want to Ensure the most relevant error message is on top
// We want to ensure the most relevant error message is on top.
if (exception.InnerException != null)
rawExceptionMessage = exception.InnerException.ToString();
else

128
Assets/Scripts/Infrastructure/UpdateSlow.cs


/// </summary>
public class UpdateSlow : MonoBehaviour, IUpdateSlow
{
[SerializeField]
[Tooltip("Update interval. Note that lobby Get requests must occur at least 1 second apart, so this period should likely be greater than that.")]
private float m_updatePeriod = 1.5f;
private class Subscriber
{
public UpdateMethod updateMethod;
public readonly float period;
public float periodCurrent;
public Subscriber(UpdateMethod updateMethod, float period)
{
this.updateMethod = updateMethod;
this.period = period;
this.periodCurrent = 0;
}
}
[SerializeField]
[Tooltip("If a subscriber to slow update takes longer than this to execute, it can be automatically unsubscribed.")]
private float m_durationToleranceMs = 10;

private List<UpdateMethod> m_subscribers = new List<UpdateMethod>();
private float m_updateTimer = 0;
private int m_nextActiveSubIndex = 0; // For staggering subscribers, to prevent spikes of lots of things triggering at once.
private List<Subscriber> m_subscribers = new List<Subscriber>();
public void Awake()
{

{
// We should clean up references in case they would prevent garbage collection.
m_subscribers.Clear();
m_subscribers.Clear(); // We should clean up references in case they would prevent garbage collection.
/// <summary>Don't assume that onUpdate will be called in any particular order compared to other subscribers.</summary>
public void Subscribe(UpdateMethod onUpdate)
/// <summary>
/// Subscribe in order to have onUpdate called approximately every period seconds (or every frame, if period <= 0).
/// Don't assume that onUpdate will be called in any particular order compared to other subscribers.
/// </summary>
public void Subscribe(UpdateMethod onUpdate, float period)
if (!m_subscribers.Contains(onUpdate))
m_subscribers.Add(onUpdate);
if (onUpdate == null)
return;
foreach (Subscriber currSub in m_subscribers)
if (currSub.updateMethod.Equals(onUpdate))
return;
m_subscribers.Add(new Subscriber(onUpdate, period));
int index = m_subscribers.IndexOf(onUpdate);
if (index >= 0)
{
m_subscribers.Remove(onUpdate);
if (index < m_nextActiveSubIndex)
m_nextActiveSubIndex--;
}
for (int sub = m_subscribers.Count - 1; sub >= 0; sub--)
if (m_subscribers[sub].updateMethod.Equals(onUpdate))
m_subscribers.RemoveAt(sub);
if (m_subscribers.Count == 0)
return;
m_updateTimer += Time.deltaTime;
float effectivePeriod = m_updatePeriod / m_subscribers.Count;
while (m_updateTimer > effectivePeriod)
{
m_updateTimer -= effectivePeriod;
OnUpdate(m_updatePeriod); // Using m_updatePeriod will be incorrect on the first update for a new subscriber, due to the staggering. However, we don't expect UpdateSlow subscribers to require precision, and this is less verbose than tracking per-subscriber.
}
OnUpdate(Time.deltaTime);
/// <summary>
/// Each frame, advance all subscribers. Any that have hit their period should then act, though if they take too long they could be removed.
/// </summary>
Stopwatch stopwatch = new Stopwatch();
m_nextActiveSubIndex = System.Math.Max(0, System.Math.Min(m_subscribers.Count - 1, m_nextActiveSubIndex)); // Just a backup.
UpdateMethod onUpdate = m_subscribers[m_nextActiveSubIndex];
if (onUpdate == null || onUpdate.Target == null) // In case something forgets to Unsubscribe when it dies.
{ Remove(m_nextActiveSubIndex, $"Did not Unsubscribe from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
return;
}
if (onUpdate.Method.ToString().Contains("<")) // Detect an anonymous or lambda or local method that cannot be Unsubscribed, by checking for a character that can't exist in a declared method name.
{ Remove(m_nextActiveSubIndex, $"Removed anonymous from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
return;
}
stopwatch.Restart();
onUpdate?.Invoke(dt);
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > m_durationToleranceMs)
for (int s = m_subscribers.Count - 1; s >= 0; s--) // Iterate in reverse in case we need to remove something.
if (!m_doNotRemoveIfTooLong)
Remove(m_nextActiveSubIndex, $"UpdateSlow subscriber took too long, removing: {onUpdate.Target} : {onUpdate.Method}");
else
var sub = m_subscribers[s];
sub.periodCurrent += Time.deltaTime;
if (sub.periodCurrent > sub.period)
Debug.LogWarning($"UpdateSlow subscriber took too long: {onUpdate.Target} : {onUpdate.Method}");
Increment();
Stopwatch stopwatch = new Stopwatch();
UpdateMethod onUpdate = sub.updateMethod;
if (onUpdate == null) // In case something forgets to Unsubscribe when it dies.
{ Remove(s, $"Did not Unsubscribe from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
if (onUpdate.Target == null) // Detect a local function that cannot be Unsubscribed since it could go out of scope.
{ Remove(s, $"Removed local function from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
if (onUpdate.Method.ToString().Contains("<")) // Detect an anonymous function that cannot be Unsubscribed, by checking for a character that can't exist in a declared method name.
{ Remove(s, $"Removed anonymous from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
stopwatch.Restart();
onUpdate?.Invoke(sub.periodCurrent);
stopwatch.Stop();
sub.periodCurrent = 0;
if (stopwatch.ElapsedMilliseconds > m_durationToleranceMs)
{
if (!m_doNotRemoveIfTooLong)
Remove(s, $"UpdateSlow subscriber took too long, removing: {onUpdate.Target} : {onUpdate.Method}");
else
Debug.LogWarning($"UpdateSlow subscriber took too long: {onUpdate.Target} : {onUpdate.Method}");
}
else
Increment();
m_nextActiveSubIndex--;
Increment();
}
void Increment()
{
m_nextActiveSubIndex++;
if (m_nextActiveSubIndex >= m_subscribers.Count)
m_nextActiveSubIndex = 0;
}
}

public interface IUpdateSlow : IProvidable<IUpdateSlow>
{
void OnUpdate(float dt);
void Subscribe(UpdateMethod onUpdate);
void Subscribe(UpdateMethod onUpdate, float period);
void Unsubscribe(UpdateMethod onUpdate);
}

public class UpdateSlowNoop : IUpdateSlow
{
public void OnUpdate(float dt) { }
public void Subscribe(UpdateMethod onUpdate) { }
public void Subscribe(UpdateMethod onUpdate, float period) { }
public void Unsubscribe(UpdateMethod onUpdate) { }
public void OnReProvided(IUpdateSlow prev) { }
}

128
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


public LobbyAsyncRequests()
{
Locator.Get.UpdateSlow.Subscribe(UpdateLobby); // Shouldn't need to unsubscribe since this instance won't be replaced.
Locator.Get.UpdateSlow.Subscribe(UpdateLobby, 0.5f); // Shouldn't need to unsubscribe since this instance won't be replaced. 0.5s is arbitrary; the rate limits are tracked later.
private Queue<Action> m_pendingOperations = new Queue<Action>();
private bool m_isMidRetrieve = false;
public Lobby CurrentLobby => m_lastKnownLobby;
public void BeginTracking(string lobbyId)

void OnComplete(Lobby lobby)
{
if (lobby != null)
{
m_isMidRetrieve = false;
HandlePendingOperations();
}
private void HandlePendingOperations()
#endregion
#region Lobby API calls are rate limited, and some other operations might want an alert when the rate limits have passed.
// Note that some APIs limit to 1 call per N seconds, while others limit to M calls per N seconds. We'll treat all APIs as though they limited to 1 call per N seconds.
public enum RequestType { Query = 0, Join }
public RateLimitCooldown GetRateLimit(RequestType type)
while (m_pendingOperations.Count > 0)
m_pendingOperations.Dequeue()?.Invoke(); // Note: If this ends up enqueuing a bunch of operations, we might need to batch them and/or ensure they don't all execute at once.
if (type == RequestType.Join)
return m_rateLimitJoin;
return m_rateLimitQuery;
private RateLimitCooldown m_rateLimitQuery = new RateLimitCooldown(1.5f); // Used for both the lobby list UI and the in-lobby updating. In the latter case, updates can be cached.
private RateLimitCooldown m_rateLimitJoin = new RateLimitCooldown(3f);
// TODO: Shift to using this to do rate limiting for all API calls? E.g. the lobby data pushing is on its own loop.
#endregion
private static Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser player)

string uasId = AuthenticationService.Instance.PlayerId;
LobbyAPIInterface.CreateLobbyAsync(uasId, lobbyName, maxPlayers, isPrivate, CreateInitialPlayerData(localUser), OnLobbyCreated);
// TODO: What about errors?
void OnLobbyCreated(Lobby response)
{
if (response == null)

/// </summary>
public void JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser, Action<Lobby> onSuccess, Action onFailure)
{
if (!m_rateLimitJoin.CanCall() ||
(lobbyId == null && lobbyCode == null))
{
onFailure?.Invoke();
// TODO: Emit some failure message.
return;
}
string uasId = AuthenticationService.Instance.PlayerId;
if (!string.IsNullOrEmpty(lobbyId))
LobbyAPIInterface.JoinLobbyAsync_ById(uasId, lobbyId, CreateInitialPlayerData(localUser), OnLobbyJoined);

/// <param name="onListRetrieved">If called with null, retrieval was unsuccessful. Else, this will be given a list of contents to display, as pairs of a lobby code and a display string for that lobby.</param>
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<QueryResponse> onError = null, LobbyColor limitToColor = LobbyColor.None)
{
if (!m_rateLimitQuery.CanCall())
{
onListRetrieved?.Invoke(null);
m_rateLimitQuery.EnqueuePendingOperation(() => { RetrieveLobbyListAsync(onListRetrieved, onError, limitToColor); });
return;
}
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Orange).ToString(), QueryFilter.OpOptions.EQ));

if (response != null)
onListRetrieved?.Invoke(response);
else
onError?.Invoke(response); // TODO: Hmm...how do we know if there was a failure?
onError?.Invoke(response);
/// <param name="onComplete">If no lobby is retrieved, this is given null.</param>
/// <param name="onComplete">If no lobby is retrieved, or if this call hits the rate limit, this is given null.</param>
if (m_isMidRetrieve)
return; // Not calling onComplete since there's just the one point at which this is called.
m_isMidRetrieve = true;
if (!m_rateLimitQuery.CanCall())
{
onComplete?.Invoke(null);
return;
}
m_isMidRetrieve = false;
onComplete?.Invoke(response);
}
}

/// </summary>
private bool ShouldUpdateData(Action caller, Action onComplete, bool shouldRetryIfLobbyNull)
{
if (m_isMidRetrieve)
{
m_pendingOperations.Enqueue(caller);
if (m_rateLimitQuery.IsInCooldown)
{ m_rateLimitQuery.EnqueuePendingOperation(caller);
m_pendingOperations.Enqueue(caller);
m_rateLimitQuery.EnqueuePendingOperation(caller);
return true;
}

m_heartbeatTime -= k_heartbeatPeriod;
LobbyAPIInterface.HeartbeatPlayerAsync(m_lastKnownLobby.Id);
}
}
public class RateLimitCooldown : Observed<RateLimitCooldown>
{
private float m_timeSinceLastCall = float.MaxValue;
private readonly float m_cooldownTime;
private Queue<Action> m_pendingOperations = new Queue<Action>();
private bool m_isHandlingPending = false; // Just in case a pending operation tries to enqueue itself again.
public void EnqueuePendingOperation(Action action)
{
if (!m_isHandlingPending)
m_pendingOperations.Enqueue(action);
}
private bool m_isInCooldown = false;
public bool IsInCooldown
{
get => m_isInCooldown;
private set
{ if (m_isInCooldown != value)
{ m_isInCooldown = value;
OnChanged(this);
}
}
}
public RateLimitCooldown(float cooldownTime)
{
m_cooldownTime = cooldownTime;
}
public bool CanCall()
{
if (m_timeSinceLastCall < m_cooldownTime)
return false;
else
{
Locator.Get.UpdateSlow.Subscribe(OnUpdate, m_cooldownTime);
m_timeSinceLastCall = 0;
IsInCooldown = true;
return true;
}
}
private void OnUpdate(float dt)
{
m_timeSinceLastCall += dt;
m_isHandlingPending = false; // (Backup in case a pending operation hit an exception.)
if (m_timeSinceLastCall >= m_cooldownTime)
{
IsInCooldown = false;
if (!m_isInCooldown) // It's possible that by setting IsInCooldown, something called CanCall immediately, in which case we want to stay on UpdateSlow.
{
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); // Note that this is after IsInCooldown is set, to prevent an Observer from kicking off CanCall again immediately.
m_isHandlingPending = true;
while (m_pendingOperations.Count > 0)
m_pendingOperations.Dequeue()?.Invoke(); // Note: If this ends up enqueuing many operations, we might need to batch them and/or ensure they don't all execute at once.
m_isHandlingPending = false;
}
}
}
public override void CopyObserved(RateLimitCooldown oldObserved) { /* This behavior isn't needed; we're just here for the OnChanged event management. */ }
}
}
}

2
Assets/Scripts/Lobby/LobbyContentHeartbeat.cs


{
m_localLobby = lobby;
m_localUser = localUser;
Locator.Get.UpdateSlow.Subscribe(OnUpdate);
Locator.Get.UpdateSlow.Subscribe(OnUpdate, 1.5f);
m_localLobby.onChanged += OnLocalLobbyChanged;
m_shouldPushData = true; // Ensure the initial presence of a new player is pushed to the lobby; otherwise, when a non-host joins, the LocalLobby never receives their data until they push something new.
}

12
Assets/Scripts/Relay/RelayAPIInterface.cs


}
catch (RelayServiceException ex)
{
Debug.LogError(ex.Message + "\n" + ex.StackTrace);
Debug.LogError($"Relay AllocateAsync returned a relay exception: {ex.Reason} - {ex.Message}");
throw;
}
}

onComplete.Invoke(joinCode);
}
catch (RelayServiceException ex)
{
Debug.LogError(ex.Message + "\n" + ex.StackTrace);
throw;
}
{
Debug.LogError($"Relay GetJoinCodeAsync returned a relay exception: {ex.Reason} - {ex.Message}");
throw;
}
}
/// <summary>

}
catch (RelayServiceException ex)
{
Debug.LogError(ex.Message + "\n" + ex.StackTrace);
Debug.LogError($"Relay JoinCodeAsync returned a relay exception: {ex.Reason} - {ex.Message}");
throw;
}
}

22
Assets/Scripts/Relay/RelayUtpClient.cs


protected List<NetworkConnection> m_connections; // For clients, this has just one member, but for hosts it will have more.
protected bool m_hasSentInitialMessage = false;
private const float k_heartbeatPeriod = 5;
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby)
{

m_networkDriver = networkDriver;
m_connections = connections;
Locator.Get.UpdateSlow.Subscribe(UpdateSlow);
Locator.Get.UpdateSlow.Subscribe(UpdateSlow, k_heartbeatPeriod);
}
protected virtual void Uninitialize()
{

{
if (m_connections.Count == 0) // This could be the case for the host alone in the lobby.
return;
m_networkDriver.ScheduleUpdate().Complete();
foreach (NetworkConnection conn in m_connections)
DoUserUpdate(m_networkDriver, conn, m_localUser);
}

protected virtual void OnUpdate()
{
m_networkDriver.ScheduleUpdate().Complete(); // This pumps all messages, which pings the Relay allocation and keeps it alive.
ReceiveNetworkEvents(m_networkDriver, m_connections);
if (!m_hasSentInitialMessage)
ReceiveNetworkEvents(m_networkDriver); // Just on the first execution, make sure to handle any events that accumulated while completing the connection.
m_networkDriver.ScheduleUpdate().Complete(); // This pumps all messages, which pings the Relay allocation and keeps it alive. It should be called no more often than ReceiveNetworkEvents.
ReceiveNetworkEvents(m_networkDriver); // This reads the message queue which was just updated.
SendInitialMessage(m_networkDriver, m_connections[0]);
SendInitialMessage(m_networkDriver, m_connections[0]); // On a client, the 0th (and only) connection is to the host.
private void ReceiveNetworkEvents(NetworkDriver driver, List<NetworkConnection> connections)
private void ReceiveNetworkEvents(NetworkDriver driver)
NetworkConnection conn;
foreach (NetworkConnection connection in connections)
while ((cmd = driver.PopEvent(out conn, out strm)) != NetworkEvent.Type.Empty) // NetworkConnection also has PopEvent, but NetworkDriver.PopEvent automatically includes new connections.
while ((cmd = connection.PopEvent(driver, out strm)) != NetworkEvent.Type.Empty)
{
ProcessNetworkEvent(connection, strm, cmd);
}
ProcessNetworkEvent(conn, strm, cmd);
}
}

4
Assets/Scripts/Relay/RelayUtpHost.cs


/// </summary>
private void DoHeartbeat()
{
m_networkDriver.ScheduleUpdate().Complete();
for (int c = m_connections.Count - 1; c >= 0; c--)
{
if (!m_connections[c].IsCreated)

{
var conn = m_networkDriver.Accept();
var conn = m_networkDriver.Accept(); // Note that since we pumped the event queue earlier in Update, m_networkDriver has been updated already this frame.
if (!conn.IsCreated) // "Nothing more to accept" is signalled by returning an invalid connection from Accept.
break;
m_connections.Add(conn);

16
Assets/Scripts/Relay/RelayUtpSetup.cs


}
protected abstract void JoinRelay();
/// <summary>
/// Shared behavior for binding to the Relay allocation, which is required for use.
/// Note that a host will send bytes from the Allocation it creates, whereas a client will send bytes from the JoinAllocation it receives using a relay code.
/// </summary>
protected void BindToAllocation(string ip, int port, byte[] allocationIdBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes, byte[] hmacKeyBytes, int connectionCapacity)
{
NetworkEndPoint serverEndpoint = NetworkEndPoint.Parse(ip, (ushort)port);

}
/// <summary>
/// Client logic: Wait until the join code is retrieved from the lobby's shared data. Then, use that code to get the Allocation to bind to, and
/// Client logic: Wait until the Relay join code is retrieved from the lobby's shared data. Then, use that code to get the Allocation to bind to, and
/// then create a connection to the host.
/// </summary>
public class RelayUtpSetupClient : RelayUtpSetup

}
}
private void OnJoin(JoinAllocation allocation)
private void OnJoin(JoinAllocation joinAllocation)
if (allocation == null)
if (joinAllocation == null)
m_allocation = allocation;
BindToAllocation(allocation.RelayServer.IpV4, allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.ConnectionData, allocation.HostConnectionData, allocation.Key, 1);
m_localLobby.RelayServer = new ServerAddress(allocation.RelayServer.IpV4, allocation.RelayServer.Port);
m_allocation = joinAllocation;
BindToAllocation(joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, joinAllocation.AllocationIdBytes, joinAllocation.ConnectionData, joinAllocation.HostConnectionData, joinAllocation.Key, 1);
m_localLobby.RelayServer = new ServerAddress(joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port);
}
protected override void OnBindingComplete()

108
Assets/Scripts/Tests/PlayMode/UpdateSlowTests.cs


{
private GameObject m_updateSlowObj;
private List<Subscriber> m_activeSubscribers = new List<Subscriber>(); // For cleaning up, in case an Assert prevents a Subscriber from taking care of itself.
private const float k_period = 1.5f;
/// <summary>Trivial Subscriber to do some action every UpdateSlow.</summary>
private class Subscriber : IDisposable

public Subscriber(Action thingToDo)
public Subscriber(Action thingToDo, float period)
Locator.Get.UpdateSlow.Subscribe(OnUpdate);
Locator.Get.UpdateSlow.Subscribe(OnUpdate, period);
m_thingToDo = thingToDo;
}

}
[UnityTest]
public IEnumerator BasicBehavior()
public IEnumerator BasicBehavior_MultipleSubs()
Subscriber sub = new Subscriber(() => { updateCount++; });
float period = 1.5f;
Subscriber sub = new Subscriber(() => { updateCount++; }, period);
yield return new WaitForSeconds(k_period - 0.1f);
yield return new WaitForSeconds(period - 0.1f);
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta.");
Assert.AreNotEqual(period, sub.prevDt, "Slow update should have received the actual amount of time that passed, not necessarily its period.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "The time delta received by slow update should match the actual time since their previous update.");
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta again.");
Assert.AreNotEqual(period, sub.prevDt, "Slow update should have received the full time delta, not just its period, again.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "The time delta received by slow update should match the actual time since their previous update, again.");
Subscriber sub2 = new Subscriber(() => { updateCount += 7; });
float period2 = period - 0.2f;
Subscriber sub2 = new Subscriber(() => { updateCount += 7; }, period2);
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta with two subscribers.");
Assert.AreEqual(1.5f, sub2.prevDt, "Slow update should have received the full time delta on the second subscriber as well.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "Slow update on the first subscriber should have received the full time delta with two subscribers.");
Assert.True(sub2.prevDt - period2 < 0.05f && sub2.prevDt - period2 > 0, "Slow update on the second subscriber should receive the actual time, even if its period is shorter.");
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
public IEnumerator BasicBehavior_UpdateEveryFrame()
{
int updateCount = 0;
Subscriber sub = new Subscriber(() => { updateCount++; }, 0);
m_activeSubscribers.Add(sub);
yield return null;
Assert.AreEqual(1, updateCount, "Update loop should update per-frame if a subscriber opts for that (#1).");
yield return null;
Assert.AreEqual(2, updateCount, "Update loop should update per-frame if a subscriber opts for that (#2).");
Assert.AreEqual(sub.prevDt, Time.deltaTime, "Subscriber should receive the correct update time since their previous update.");
sub.Dispose();
yield return new WaitForSeconds(0.5f);
Assert.AreEqual(2, updateCount, "Should have unsubscribed the subscriber.");
}
[UnityTest]
Locator.Get.UpdateSlow.Subscribe((dt) => { updateCount++; });
float period = 0.5f;
Locator.Get.UpdateSlow.Subscribe((dt) => { updateCount++; }, period);
yield return new WaitForSeconds(k_period + 0.1f);
yield return new WaitForSeconds(period + 0.1f);
Locator.Get.UpdateSlow.Subscribe(ThisIsALocalFunction, period);
LogAssert.Expect(LogType.Error, new Regex(".*Removed local function.*"));
yield return new WaitForSeconds(period + 0.1f);
Assert.AreEqual(0, updateCount, "Local functions should not be permitted, since they can't be Unsubscribed.");
void ThisIsALocalFunction(float dt) { }
public IEnumerator StaggerClients()
public IEnumerator SubscribeNoDuplicates()
int updateCountA = 0, updateCountB = 0;
Subscriber subA = new Subscriber(() => { updateCountA++; });
Subscriber subB = new Subscriber(() => { updateCountB++; });
m_activeSubscribers.Add(subA);
m_activeSubscribers.Add(subB);
float periodHalf = k_period / 2;
yield return new WaitForSeconds(periodHalf - 0.1f);
Assert.AreEqual(0, updateCountA, "Base case (count A)");
Assert.AreEqual(0, updateCountB, "Base case (count B)");
yield return new WaitForSeconds(0.2f);
Assert.AreEqual(1, updateCountA, "Updates are now on half the normal period. First update is first.");
Assert.AreEqual(0, updateCountB, "Updates are now on half the normal period. Second update is second.");
dummyOnUpdateCalls = 0;
Locator.Get.UpdateSlow.Subscribe(DummyOnUpdate, 1);
Locator.Get.UpdateSlow.Subscribe(DummyOnUpdate, 0.1f);
yield return new WaitForSeconds(periodHalf);
Assert.AreEqual(1, updateCountA, "First update is still offset.");
Assert.AreEqual(1, updateCountB, "Second update should hit now.");
yield return new WaitForSeconds(0.9f);
Assert.AreEqual(0, dummyOnUpdateCalls, "The second Subscribe call should not have gone through.");
subB.Dispose();
yield return new WaitForSeconds(periodHalf);
Assert.AreEqual(1, updateCountA, "First update should no longer be offset.");
Assert.AreEqual(1, updateCountB, "Second update is unsubscribed.");
yield return new WaitForSeconds(0.2f);
Assert.AreEqual(1, dummyOnUpdateCalls, "The first Subscribe call should have gone through.");
yield return new WaitForSeconds(periodHalf);
Assert.AreEqual(2, updateCountA, "First update should hit with normal timing.");
Assert.AreEqual(1, updateCountB, "Second update is still unsubscribed.");
Locator.Get.UpdateSlow.Unsubscribe(DummyOnUpdate);
yield return new WaitForSeconds(1);
Assert.AreEqual(1, dummyOnUpdateCalls, "Unsubscribe should work as expected.");
private int dummyOnUpdateCalls = 0;
private void DummyOnUpdate(float dt) { dummyOnUpdateCalls++; }
[UnityTest]
public IEnumerator WhatIfASubscriberIsVerySlow()

float period = 1.5f;
});
}, period);
yield return new WaitForSeconds(k_period + 0.1f);
yield return new WaitForSeconds(period + 0.1f);
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
Assert.AreEqual(1, updateCount, "Should have removed the offending subscriber.");
}
}

2
Assets/Scripts/UI/JoinMenuUI.cs


m_LocalLobbySelected = lobby.Data;
}
public void OnJoinCodeInputFieldChanged(string newCode)
public void OnLobbyCodeInputFieldChanged(string newCode)
{
if (!string.IsNullOrEmpty(newCode))
m_LocalLobbySelected = new LocalLobby.LobbyData(newCode.ToUpper());

12
Assets/Scripts/UI/SpinnerUI.cs


{
spinnerImage.Hide();
errorTextVisibility.Show();
var errorString = new StringBuilder();
errorString.Append("Error");
errorString.Append(": ");
if (observed.lastErrorCode > 0)
{
errorString.Append(observed.lastErrorCode);
errorString.Append(", ");
}
errorString.Append("Check Unity Console Log for Details.");
errorText.SetText(errorString.ToString());
errorText.SetText("Error. See Unity Console log for details.");
}
else if (observed.State == LobbyQueryState.Fetched)
{

11
Assets/Scripts/UI/UIPanelBase.cs


public class UIPanelBase : MonoBehaviour
{
[SerializeField]
protected UnityEvent<bool> m_onVisibilityChange;
private UnityEvent<bool> m_onVisibilityChange;
bool showing;
CanvasGroup m_canvasGroup;

child.m_onVisibilityChange?.Invoke(true);
}
public void Hide()
public void Hide() // Called by some serialized events, so we can't just have targetAlpha as an optional parameter.
{
Hide(0);
}
public void Hide(float targetAlpha)
MyCanvasGroup.alpha = 0;
MyCanvasGroup.alpha = targetAlpha;
MyCanvasGroup.interactable = false;
MyCanvasGroup.blocksRaycasts = false;
showing = false;

35
Assets/Scripts/UI/RateLimitVisibility.cs


using UnityEngine;
namespace LobbyRelaySample.UI
{
/// <summary>
/// Observes the Lobby request rate limits and changes the visibility of a UIPanelBase to suit.
/// E.g. the refresh button on the Join menu should be inactive after a refresh for long enough to avoid the lobby query rate limit.
/// </summary>
public class RateLimitVisibility : MonoBehaviour
{
[SerializeField]
UIPanelBase m_target;
[SerializeField]
float m_alphaWhenHidden = 0.5f;
[SerializeField]
LobbyAsyncRequests.RequestType m_requestType;
private void Start()
{
LobbyAsyncRequests.Instance.GetRateLimit(m_requestType).onChanged += UpdateVisibility;
}
private void OnDestroy()
{
LobbyAsyncRequests.Instance.GetRateLimit(m_requestType).onChanged -= UpdateVisibility;
}
private void UpdateVisibility(LobbyAsyncRequests.RateLimitCooldown rateLimit)
{
if (rateLimit.IsInCooldown)
m_target.Hide(m_alphaWhenHidden);
else
m_target.Show();
}
}
}

11
Assets/Scripts/UI/RateLimitVisibility.cs.meta


fileFormatVersion: 2
guid: 771ddda4ce2ee2a4dad57866ecc1170d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存