浏览代码

merged in Async teardown

/main/staging/wire_implementation
当前提交
2ca155b9
共有 143 个文件被更改,包括 3719 次插入2077 次删除
  1. 61
      Assets/Prefabs/GameManager.prefab
  2. 31
      Assets/Prefabs/UI/BackButtonBG.prefab
  3. 70
      Assets/Prefabs/UI/GameCanvas.prefab
  4. 80
      Assets/Prefabs/UI/JoinContent.prefab
  5. 65
      Assets/Prefabs/UI/JoinCreateCanvas.prefab
  6. 109
      Assets/Prefabs/UI/LobbyCanvas.prefab
  7. 34
      Assets/Prefabs/UI/LobbyCodeCanvas.prefab
  8. 48
      Assets/Prefabs/UI/LobbyUserList.prefab
  9. 41
      Assets/Prefabs/UI/MainMenuCanvas.prefab
  10. 34
      Assets/Prefabs/UI/RelayCodeCanvas.prefab
  11. 35
      Assets/Prefabs/UI/SpinnerUI.prefab
  12. 84
      Assets/Prefabs/UI/UserInteractionPanel.prefab
  13. 384
      Assets/Scenes/mainScene.unity
  14. 2
      Assets/Scripts/GameLobby/Auth/Auth.cs.meta
  15. 49
      Assets/Scripts/GameLobby/Game/Countdown.cs
  16. 462
      Assets/Scripts/GameLobby/Game/GameManager.cs
  17. 4
      Assets/Scripts/GameLobby/Game/LobbyUserObserver.cs
  18. 353
      Assets/Scripts/GameLobby/Game/LocalLobby.cs
  19. 22
      Assets/Scripts/GameLobby/Game/ServerAddress.cs
  20. 7
      Assets/Scripts/GameLobby/Infrastructure/Locator.cs
  21. 2
      Assets/Scripts/GameLobby/Infrastructure/Messenger.cs
  22. 4
      Assets/Scripts/GameLobby/Infrastructure/Observed.cs
  23. 2
      Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs
  24. 14
      Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs
  25. 148
      Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs
  26. 8
      Assets/Scripts/GameLobby/LobbyRelaySample.asmdef
  27. 6
      Assets/Scripts/GameLobby/NGO/InGameRunner.cs
  28. 4
      Assets/Scripts/GameLobby/NGO/IntroOutroRunner.cs
  29. 14
      Assets/Scripts/GameLobby/NGO/NetworkedDataStore.cs
  30. 4
      Assets/Scripts/GameLobby/NGO/PlayerCursor.cs
  31. 8
      Assets/Scripts/GameLobby/NGO/ResultsUserUI.cs
  32. 10
      Assets/Scripts/GameLobby/NGO/Scorer.cs
  33. 111
      Assets/Scripts/GameLobby/NGO/SetupInGame.cs
  34. 10
      Assets/Scripts/GameLobby/NGO/SymbolContainer.cs
  35. 4
      Assets/Scripts/GameLobby/NGO/SymbolKillVolume.cs
  36. 4
      Assets/Scripts/GameLobby/NGO/SymbolObject.cs
  37. 2
      Assets/Scripts/GameLobby/Relay/AsyncRequestRelay.cs
  38. 2
      Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs
  39. 50
      Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs
  40. 81
      Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs
  41. 468
      Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs
  42. 8
      Assets/Scripts/GameLobby/Tests/Editor/MessengerTests.cs
  43. 4
      Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs
  44. 148
      Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs
  45. 127
      Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs
  46. 4
      Assets/Scripts/GameLobby/Tests/PlayMode/Tests.Play.asmdef
  47. 24
      Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs
  48. 11
      Assets/Scripts/GameLobby/UI/CountdownUI.cs
  49. 12
      Assets/Scripts/GameLobby/UI/CreateMenuUI.cs
  50. 30
      Assets/Scripts/GameLobby/UI/DisplayCodeUI.cs
  51. 2
      Assets/Scripts/GameLobby/UI/EmoteButtonUI.cs
  52. 24
      Assets/Scripts/GameLobby/UI/GameStateVisibilityUI.cs
  53. 30
      Assets/Scripts/GameLobby/UI/InLobbyUserList.cs
  54. 65
      Assets/Scripts/GameLobby/UI/InLobbyUserUI.cs
  55. 22
      Assets/Scripts/GameLobby/UI/JoinCreateLobbyUI.cs
  56. 40
      Assets/Scripts/GameLobby/UI/JoinMenuUI.cs
  57. 26
      Assets/Scripts/GameLobby/UI/LobbyButtonUI.cs
  58. 7
      Assets/Scripts/GameLobby/UI/LobbyNameUI.cs
  59. 12
      Assets/Scripts/GameLobby/UI/LobbyUserVolumeUI.cs
  60. 56
      Assets/Scripts/GameLobby/UI/RateLimitVisibility.cs
  61. 2
      Assets/Scripts/GameLobby/UI/ReadyCheckUI.cs
  62. 16
      Assets/Scripts/GameLobby/UI/RecolorForLobbyType.cs
  63. 14
      Assets/Scripts/GameLobby/UI/RelayAddressUI.cs
  64. 53
      Assets/Scripts/GameLobby/UI/ShowWhenLobbyStateUI.cs
  65. 38
      Assets/Scripts/GameLobby/UI/SpinnerUI.cs
  66. 12
      Assets/Scripts/GameLobby/UI/UIPanelBase.cs
  67. 15
      Assets/Scripts/GameLobby/UI/UserNameUI.cs
  68. 42
      Assets/Scripts/GameLobby/UI/UserStateVisibilityUI.cs
  69. 3
      Assets/Scripts/GameLobby/Vivox/VivoxSetup.cs
  70. 37
      Packages/manifest.json
  71. 91
      Packages/packages-lock.json
  72. 15
      ProjectSettings/PackageManagerSettings.asset
  73. 2
      ProjectSettings/RiderScriptEditorPersistedState.asset
  74. 2
      Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs.meta
  75. 118
      Assets/Scripts/GameLobby/Auth/Auth.cs
  76. 44
      Assets/Scripts/GameLobby/Game/LocalLobbyList.cs
  77. 55
      Assets/Scripts/GameLobby/Game/LocalPlayer.cs
  78. 39
      Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs
  79. 409
      Assets/Scripts/GameLobby/Lobby/LobbyManager.cs
  80. 193
      Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs
  81. 3
      Packages/ParrelSync.meta
  82. 167
      ProjectSettings/SceneTemplateSettings.json
  83. 8
      Packages/ParrelSync/Editor.meta
  84. 10
      Packages/ParrelSync/package.json
  85. 7
      Packages/ParrelSync/package.json.meta
  86. 15
      Packages/ParrelSync/projectCloner.asmdef
  87. 7
      Packages/ParrelSync/projectCloner.asmdef.meta
  88. 8
      Packages/ParrelSync/Editor/AssetModBlock.meta
  89. 22
      Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs
  90. 11
      Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta
  91. 34
      Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs
  92. 11
      Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta
  93. 664
      Packages/ParrelSync/Editor/ClonesManager.cs
  94. 11
      Packages/ParrelSync/Editor/ClonesManager.cs.meta
  95. 11
      Packages/ParrelSync/Editor/ClonesManagerWindow.cs.meta
  96. 13
      Packages/ParrelSync/Editor/ExternalLinks.cs
  97. 11
      Packages/ParrelSync/Editor/ExternalLinks.cs.meta

61
Assets/Prefabs/GameManager.prefab


m_Component:
- component: {fileID: 802462301182945455}
- component: {fileID: 6265861362966661484}
- component: {fileID: 5235782363599194820}
- component: {fileID: 2812013971948110896}
m_Layer: 0
m_Name: SetupInGame
m_TagString: Untagged

m_Script: {fileID: 11500000, guid: 3f7533ddeca587549a9798a65a8670ba, type: 3}
m_Name:
m_EditorClassIdentifier:
m_NetworkManagerPrefab: {fileID: 5021303663353436182, guid: b963f71e4874d4066bc72b9224e3ffce, type: 3}
--- !u!114 &5235782363599194820
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5395715167687395500}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 6265861362966661484}
m_TargetAssemblyTypeName: LobbyRelaySample.inGame.SetupInGame, LobbyRelaySample
m_MethodName: OnLobbyChange
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &2812013971948110896
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5395715167687395500}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 6265861362966661484}
m_TargetAssemblyTypeName: LobbyRelaySample.inGame.SetupInGame, LobbyRelaySample
m_MethodName: OnLocalUserChange
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!1 &7716713811812636896
GameObject:
m_ObjectHideFlags: 0

m_Script: {fileID: 11500000, guid: b4f7225f73bfe6a4d9133ee45ac9cd73, type: 3}
m_Name:
m_EditorClassIdentifier:
m_LocalMenuStateObservers: []
m_LobbyServiceObservers: []
m_setupInGame: {fileID: 6265861362966661484}
m_countdown: {fileID: 0}
m_vivoxUserHandlers: []
--- !u!114 &5193415626965589893
MonoBehaviour:

31
Assets/Prefabs/UI/BackButtonBG.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8687266200334079465}
m_Father: {fileID: 21306902101918852}

- component: {fileID: 9211534540060107035}
- component: {fileID: 3548897939371032169}
- component: {fileID: 7262104930290195570}
- component: {fileID: 8907220659739637781}
- component: {fileID: 7728157518251256520}
m_Layer: 5
m_Name: BackButtonBG

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6522473138835034473}
m_Father: {fileID: 0}

m_Script: {fileID: 11500000, guid: 3c93e82eef7d613418b85194aace7f69, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &8907220659739637781
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5416637149047939654}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 7728157518251256520}
m_TargetAssemblyTypeName: LobbyRooms.UI.GameStateVisibilityUI, LobbyRooms
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &7728157518251256520
MonoBehaviour:
m_ObjectHideFlags: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6522473138835034473}
m_RootOrder: 0

70
Assets/Prefabs/UI/GameCanvas.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3862000843821052302}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3168212744299139599}
m_Father: {fileID: 5323557791684491924}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1166753073431177940}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4033840405646935}
- {fileID: 8211845410637438851}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8651475152227732998}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3933490046232978046}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3513144943005353968}
m_Father: {fileID: 5323557791684491924}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5265762631451841679}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8628454958398271548}
m_Father: {fileID: 2437121395950827588}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6891852016103666983}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8628454959107311804}
- {fileID: 8628454957908509635}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2017918237437651757}
- {fileID: 2761609676931020092}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8578661244454260921}
m_RootOrder: 0

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_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4100836674054655770}
- {fileID: 5323557791684491924}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9059265328890417471}
m_RootOrder: 0

m_RemovedComponents:
- {fileID: 418819164531713121, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_SourcePrefab: {fileID: 100100000, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!224 &3933490046232978046 stripped
--- !u!224 &1166753073431177940 stripped
m_CorrespondingSourceObject: {fileID: 6777302673485755179, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_CorrespondingSourceObject: {fileID: 8694871130870774657, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!224 &8578661244454260921 stripped
--- !u!224 &3862000843821052302 stripped
m_CorrespondingSourceObject: {fileID: 2276224879528722924, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_CorrespondingSourceObject: {fileID: 6702435737022390491, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!224 &6891852016103666983 stripped
--- !u!224 &3933490046232978046 stripped
m_CorrespondingSourceObject: {fileID: 3981048342783665266, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_CorrespondingSourceObject: {fileID: 6777302673485755179, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!224 &9059265328890417471 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 1523264875846598762, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!114 &4174114467305540031 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5869930269783152874, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &1166753073431177940 stripped
--- !u!224 &6891852016103666983 stripped
m_CorrespondingSourceObject: {fileID: 8694871130870774657, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_CorrespondingSourceObject: {fileID: 3981048342783665266, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!224 &3862000843821052302 stripped
--- !u!224 &8578661244454260921 stripped
m_CorrespondingSourceObject: {fileID: 6702435737022390491, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_CorrespondingSourceObject: {fileID: 2276224879528722924, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!224 &8651475152227732998 stripped
--- !u!224 &8628454958398271548 stripped
m_CorrespondingSourceObject: {fileID: 1192176632984182611, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_CorrespondingSourceObject: {fileID: 2244251207921394025, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!114 &4174114467305540031 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5869930269783152874, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
--- !u!224 &8651475152227732998 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 1192176632984182611, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &8628454958398271548 stripped
--- !u!224 &9059265328890417471 stripped
m_CorrespondingSourceObject: {fileID: 2244251207921394025, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_CorrespondingSourceObject: {fileID: 1523264875846598762, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
m_PrefabInstance: {fileID: 7537689341060714837}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &8628454958156142197

propertyPath: m_Name
value: RenameButtonCanvas
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedComponents:
- {fileID: 3817649292031598177, guid: 0828349f6319d084bbba8edd08991e62, type: 3}
m_SourcePrefab: {fileID: 100100000, guid: 0828349f6319d084bbba8edd08991e62, type: 3}
--- !u!224 &3513144943005353968 stripped
RectTransform:

80
Assets/Prefabs/UI/JoinContent.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3810288654714679559}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 476776760980644771}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9152075987234541224}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8242294458145102565}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7955209533444849375}
m_RootOrder: 0

- component: {fileID: 785260762106121641}
- component: {fileID: 3931324176504405867}
- component: {fileID: 1462126939442648229}
- component: {fileID: 7550446569341709048}
m_Layer: 5
m_Name: JoinContent
m_TagString: Untagged

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4325310634866704869}
- {fileID: 7970272424148618348}

m_LobbyButtonPrefab: {fileID: 7018369548608736188, guid: f6d35a456ba76a24587dce83bd088b7d, type: 3}
m_LobbyButtonParent: {fileID: 7824921818678239159}
m_JoinCodeField: {fileID: 8659642538454988273}
--- !u!114 &7550446569341709048
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 785260762106121644}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: edfa4d53eaae84c43ba581088940700c, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1462126939442648229}
m_TargetAssemblyTypeName: LobbyRooms.UI.JoinMenuUI, LobbyRooms
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
m_JoinCreateLobbyUI: {fileID: 0}
--- !u!1 &891284586109156510
GameObject:
m_ObjectHideFlags: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3864498296333289704}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3864498296333289704}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4279971184503703239}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2072554153277161701}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5676572735189107223}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1839420744844123086}
- {fileID: 4260256627374184318}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 785260762106121647}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8298271775641329263}
- {fileID: 7143473272919870158}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 476776760980644771}
m_Father: {fileID: 4100995143772770972}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9095321446247494771}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4079924192455176000}
- {fileID: 6265314097236686696}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4260256627374184318}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3632021558847075417}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3755762060919842715}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5676572735189107223}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4100995143772770972}
m_Father: {fileID: 4325310634866704869}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9152075987234541224}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3755762060919842715}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3810288654714679559}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2226373431972622546}
- {fileID: 7022454495411658382}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2072554153277161701}
- {fileID: 4178278345133798937}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7573825319354851387}
m_Father: {fileID: 1604043255686064399}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7955209533444849375}
- {fileID: 1604043255686064399}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 796953684453886289}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2735345665116889010}
- {fileID: 8807990684132234493}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3864498296333289704}
- {fileID: 8941623568823021572}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2072554153277161701}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2716431726230942877}
- {fileID: 4270049585292890095}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 796953684453886289}
- {fileID: 3165760063825440603}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4036064834031661316}
- {fileID: 7485750802846073613}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3309229614039675927}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4200763249476693650}
- {fileID: 4051350118216901408}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 796953684453886289}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4279971184503703239}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7315760059761538639}
m_Father: {fileID: 2826622903624280382}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 9152075987234541224}
- {fileID: 6267811842759446846}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8891399108302556007}
- {fileID: 1608448316400021619}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7824921818678239159}
- {fileID: 8814352389997950082}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3075017331089573731}
m_Father: {fileID: 4100995143772770972}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7545758080679127068}
- {fileID: 7191491465445676253}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3075017331089573731}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5676572735189107223}
- {fileID: 9095321446247494771}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1839420744844123086}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8754000458150487275}
- {fileID: 2344857208759781651}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3810288654714679559}
- {fileID: 6936709559246390327}

65
Assets/Prefabs/UI/JoinCreateCanvas.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8221895778010294642}
m_Father: {fileID: 5919863887503833647}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6742761257817967520}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1659773181924692514}
m_Father: {fileID: 5919863887503833647}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7885199827131071637}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3691881777428488015}
- {fileID: 5836614389469498580}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8579668188475321874}
m_Father: {fileID: 5919863887503833647}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8636295960037856115}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5919863887503833647}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5919863887503833647}
- {fileID: 1119140321553661053}

- component: {fileID: 5836614391142406753}
- component: {fileID: 5836614391142406752}
- component: {fileID: 4578721078997909056}
- component: {fileID: 1512606419251751951}
- component: {fileID: 6102798993520257211}
m_Layer: 5
m_Name: JoinCreateCanvas

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5836614390199223663}
m_Father: {fileID: 0}

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
m_OnTabChanged:
m_PersistentCalls:
m_Calls: []
--- !u!114 &1512606419251751951
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5836614391142406755}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 4578721078997909056}
m_TargetAssemblyTypeName: LobbyRooms.UI.JoinCreateRoomUI, LobbyRooms
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!225 &6102798993520257211
CanvasGroup:
m_ObjectHideFlags: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5919863887503833647}
m_RootOrder: 4

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8015712818246554090}
- {fileID: 6742761257817967520}

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
--- !u!224 &5836614389469498580 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 785260762106121647, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
m_PrefabInstance: {fileID: 6492536299820417403}
m_PrefabAsset: {fileID: 0}
--- !u!114 &5643181761482593758 stripped
--- !u!114 &2465479314273514634 stripped
m_CorrespondingSourceObject: {fileID: 1462126939442648229, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
m_CorrespondingSourceObject: {fileID: 8659642538454988273, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
m_Script: {fileID: 11500000, guid: 3b8c744e110596042b40ee73862efaab, type: 3}
m_Script: {fileID: 11500000, guid: 2da0c512f12947e489f739169773d7ca, type: 3}
--- !u!114 &2465479314273514634 stripped
--- !u!114 &5643181761482593758 stripped
m_CorrespondingSourceObject: {fileID: 8659642538454988273, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
m_CorrespondingSourceObject: {fileID: 1462126939442648229, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
m_Script: {fileID: 11500000, guid: 2da0c512f12947e489f739169773d7ca, type: 3}
m_Script: {fileID: 11500000, guid: 3b8c744e110596042b40ee73862efaab, type: 3}
--- !u!224 &5836614389469498580 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 785260762106121647, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
m_PrefabInstance: {fileID: 6492536299820417403}
m_PrefabAsset: {fileID: 0}

109
Assets/Prefabs/UI/LobbyCanvas.prefab


m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1906352097507614706}
m_RootOrder: 1

m_Component:
- component: {fileID: 2244251207921394025}
- component: {fileID: 5434623004655309427}
- component: {fileID: 3687075862038422248}
- component: {fileID: 3403950992349691351}
m_Layer: 5
m_Name: LobbyCanvas

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6520888008628790540}
- {fileID: 2244251208239780121}

m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!114 &3687075862038422248
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2244251207921394026}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 3403950992349691351}
m_TargetAssemblyTypeName: LobbyRooms.GameStatePanelUI, Assembly-CSharp
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &3403950992349691351
MonoBehaviour:
m_ObjectHideFlags: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 466693923092094802}
- {fileID: 1529153573702653751}

m_Component:
- component: {fileID: 466693923092094802}
- component: {fileID: 2177115228008557851}
- component: {fileID: 5843558592166708181}
- component: {fileID: 6860436446719602335}
- component: {fileID: 6749879306276389991}
m_Layer: 5

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4212636328457333019}
m_Father: {fileID: 6520888008628790540}

m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4637522307789944801}
m_CullTransparentMesh: 1
--- !u!114 &5843558592166708181
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4637522307789944801}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 6860436446719602335}
m_TargetAssemblyTypeName: LobbyRooms.UI.ServerNameUI, Assembly-CSharp
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &6860436446719602335
MonoBehaviour:
m_ObjectHideFlags: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1906352097507614706}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 466693923092094802}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1906352097507614706}
- {fileID: 2957117094117392419}

m_Component:
- component: {fileID: 1906352097507614706}
- component: {fileID: 8834330287924717091}
- component: {fileID: 7526558453095601547}
- component: {fileID: 279783410280127446}
- component: {fileID: 8630015524497407890}
m_Layer: 5

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6532118317105062884}
- {fileID: 7445706979321241619}

m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7079534792968695919}
m_CullTransparentMesh: 1
--- !u!114 &7526558453095601547
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7079534792968695919}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 279783410280127446}
m_TargetAssemblyTypeName: LobbyRooms.UI.ServerAddressUI, LobbyRooms
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &279783410280127446
MonoBehaviour:
m_ObjectHideFlags: 0

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedComponents:
- {fileID: 6887166789349504240, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- {fileID: 3235578093204888586, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- {fileID: 2586128159150077185, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- {fileID: 2853056286508716223, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- {fileID: 8850815072349184999, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
m_SourcePrefab: {fileID: 100100000, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
--- !u!224 &3566036481622844614 stripped
RectTransform:

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
--- !u!224 &1529153573702653751 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 1118541987231860827, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
m_PrefabInstance: {fileID: 1926829325626138476}
m_PrefabAsset: {fileID: 0}
--- !u!114 &2768469221279893250 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 4383951900022528110, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}

m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &1529153573702653751 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 1118541987231860827, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
m_PrefabInstance: {fileID: 1926829325626138476}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &2510549639321509767
PrefabInstance:
m_ObjectHideFlags: 0

34
Assets/Prefabs/UI/LobbyCodeCanvas.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7356978206806771932}
m_RootOrder: 1

- component: {fileID: 1118541987231860831}
- component: {fileID: 6772704612072308136}
- component: {fileID: 1118541987231860826}
- component: {fileID: 699060394989383769}
m_Layer: 5
m_Name: LobbyCodeCanvas
m_TagString: Untagged

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1060855158700719950}
- {fileID: 7974420753511511685}

m_Calls: []
m_outputText: {fileID: 5578852939709204548}
m_codeType: 0
--- !u!114 &699060394989383769
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1118541987231860824}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1118541987231860826}
m_TargetAssemblyTypeName: LobbyRooms.UI.RoomCodeUI, LobbyRooms
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!1 &2798863108443093305
GameObject:
m_ObjectHideFlags: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7356978206806771932}
m_Father: {fileID: 1118541987231860827}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7356978206806771932}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 666358594839851073}
- {fileID: 2985101570411328694}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1118541987231860827}
m_RootOrder: 0

48
Assets/Prefabs/UI/LobbyUserList.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8268348179057556550}
m_Father: {fileID: 8316201547685935311}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1550284535874684459}
m_Father: {fileID: 8316201547685935311}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7187078656748750641}
- {fileID: 8594180776620219635}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4463750083940306590}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6491466947543783896}
m_Father: {fileID: 8316201547685935311}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 9211655745301838380}
- {fileID: 2630686559100583219}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3685075291402800769}
m_Father: {fileID: 8316201547685935311}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8316201547685935311}
m_Father: {fileID: 4463750083940306590}

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
--- !u!224 &3685075291402800769 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 2300164376451760337}
m_PrefabAsset: {fileID: 0}
--- !u!114 &6290668908104815487 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5235189765028238254, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}

m_Script: {fileID: 11500000, guid: 9557a5d232068a149987bc0753800f26, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &3685075291402800769 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 2300164376451760337}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &4128781293466929275
PrefabInstance:
m_ObjectHideFlags: 0

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
--- !u!224 &1550284535874684459 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 4128781293466929275}
m_PrefabAsset: {fileID: 0}
--- !u!114 &8208727230702623701 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5235189765028238254, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}

m_Script: {fileID: 11500000, guid: 9557a5d232068a149987bc0753800f26, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &1550284535874684459 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 4128781293466929275}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &6805156706942808598
PrefabInstance:
m_ObjectHideFlags: 0

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
--- !u!224 &8268348179057556550 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 6805156706942808598}
m_PrefabAsset: {fileID: 0}
--- !u!114 &1646035575854440888 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5235189765028238254, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}

m_Script: {fileID: 11500000, guid: 9557a5d232068a149987bc0753800f26, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &8268348179057556550 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 6805156706942808598}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &8564023431761303432
PrefabInstance:
m_ObjectHideFlags: 0

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
--- !u!224 &6491466947543783896 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 8564023431761303432}
m_PrefabAsset: {fileID: 0}
--- !u!114 &4503217775887893542 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5235189765028238254, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}

m_Script: {fileID: 11500000, guid: 9557a5d232068a149987bc0753800f26, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &6491466947543783896 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 3229036008637484624, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
m_PrefabInstance: {fileID: 8564023431761303432}
m_PrefabAsset: {fileID: 0}

41
Assets/Prefabs/UI/MainMenuCanvas.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3838875500213396203}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1547097154335127670}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1547097154637997989}
m_RootOrder: 1

- component: {fileID: 1547097154335127667}
- component: {fileID: 1130581240334700700}
- component: {fileID: 5258679254951932468}
- component: {fileID: 575636749548035060}
m_Layer: 5
m_Name: MainMenuCanvas
m_TagString: Untagged

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1547097153480224111}
- {fileID: 1547097155015051324}

m_PersistentCalls:
m_Calls: []
ShowThisWhen: 1
--- !u!114 &575636749548035060
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1547097154335127666}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 5258679254951932468}
m_TargetAssemblyTypeName: LobbyRooms.GameStateVisibilityUI, Assembly-CSharp
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!1 &1547097154637997988
GameObject:
m_ObjectHideFlags: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 32978559587113065}
- {fileID: 1547097153987463586}

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: [{fileID: 1547097154637997989}, {fileID: 3838875500213396203}]
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1547097154637997989}
- {fileID: 3838875500213396203}
m_Father: {fileID: 1547097154335127670}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2256322442099666709}
- {fileID: 3557122422878236895}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1547097154637997989}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3838875500213396203}
m_RootOrder: 1

34
Assets/Prefabs/UI/RelayCodeCanvas.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5867107070128564602}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4488536198637490663}
- {fileID: 2168532048885830928}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5867107070128564602}
m_Father: {fileID: 4102997489641105917}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4102997489641105917}
m_RootOrder: 0

- component: {fileID: 4102997489641105913}
- component: {fileID: 7676491730539518990}
- component: {fileID: 3340928240658051873}
- component: {fileID: 4523467532116611583}
m_Layer: 5
m_Name: RelayCodeCanvas
m_TagString: Untagged

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5389159257502379616}
- {fileID: 6476122475963766563}

m_Calls: []
m_outputText: {fileID: 8798075752901962210}
m_codeType: 1
--- !u!114 &4523467532116611583
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4102997489641105918}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 3340928240658051873}
m_TargetAssemblyTypeName: LobbyRelaySample.UI.DisplayCodeUI, LobbyRelaySample
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!1 &4142193733183565639
GameObject:
m_ObjectHideFlags: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5867107070128564602}
m_RootOrder: 1

35
Assets/Prefabs/UI/SpinnerUI.prefab


m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8141644855275361747}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8141644855275361747}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8141644855275361747}
m_RootOrder: 2

- component: {fileID: 8141644855275361747}
- component: {fileID: 8024795096463497524}
- component: {fileID: 6317050540274997421}
- component: {fileID: 2592976293271768794}
- component: {fileID: 7469848848124085693}
- component: {fileID: 3773418773225383442}
m_Layer: 5

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2952266133262597715}
- {fileID: 8845592316474837655}

m_noServerText: {fileID: 1576294672583716881}
m_errorTextVisibility: {fileID: 8041986936720407558}
m_raycastBlocker: {fileID: 0}
--- !u!114 &2592976293271768794
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7144088886657378797}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: edfa4d53eaae84c43ba581088940700c, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 6317050540274997421}
m_TargetAssemblyTypeName: LobbyRooms.UI.SpinnerUI, Assembly-CSharp
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
serializedVersion: 3
serializedVersion: 4
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}

m_UpdateMode: 0
m_ApplyRootMotion: 0
m_LinearVelocityBlending: 0
m_StabilizeFeet: 0
m_WarningMessage:
m_HasTransformHierarchy: 1
m_AllowConstantClipSamplingOptimization: 1

84
Assets/Prefabs/UI/UserInteractionPanel.prefab


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: 0.999}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2119950809865779987}
m_Father: {fileID: 6664205945102926799}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 474140199862162053}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1611213509401803489}
m_RootOrder: 2

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8979361099148208042}
- {fileID: 1135759803389522016}

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: 0.999}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1135759803389522016}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8894954517847697079}
- {fileID: 506300669760714432}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6979352419326739443}
- {fileID: 7173726187766461959}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4215597292682722394}
- {fileID: 2758617697121517356}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6541171577144360681}
m_RootOrder: 1

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: 0.999}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4315664788983455717}
m_Father: {fileID: 1919168897190896396}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1309712272618711075}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1135759803389522016}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1611213509401803489}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5151586559654887469}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6870502395855781705}
- {fileID: 6666831914584290858}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4191270325501815023}
- {fileID: 1770059644204541294}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 233260285268708796}
m_Father: {fileID: 5026269005358103012}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2848144350161010536}
- {fileID: 1150714971814560546}

m_Component:
- component: {fileID: 794717087570398863}
- component: {fileID: 2987822160017223264}
- component: {fileID: 26462937623057251}
m_Layer: 5
m_Name: ToggleContainer
m_TagString: Untagged

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5272370078138508777}
m_Father: {fileID: 5151586559654887469}

- {fileID: 5621563591146875997}
- {fileID: 4807901211461671397}
- {fileID: 6687484736792569641}
--- !u!114 &26462937623057251
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2945500889587214856}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 2987822160017223264}
m_TargetAssemblyTypeName: LobbyRelaySample.UI.RecolorForLobbyType, LobbyRelaySample
m_MethodName: UpdateLobby
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!1 &2988716332856923472
GameObject:
m_ObjectHideFlags: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6004833744538984969}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1.02, y: 1.02, z: 1.02}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2848144350161010536}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3197502779878208215}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2677764702249492100}
- {fileID: 1568259630799150366}

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: 0.999}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1309712272618711075}
m_RootOrder: 0

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5543134037343114586}
- {fileID: 794717087570398863}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3620679801590011701}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 474140199862162053}
m_Father: {fileID: 5026269005358103012}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6075935838108688747}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2056817220376623591}
- {fileID: 5151586559654887469}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6004833744538984969}
m_Father: {fileID: 4558362294547660329}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 759037105273515699}
- {fileID: 1108938163892239274}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 233260285268708796}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5671183532491584052}
m_Father: {fileID: 5026269005358103012}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8540831390494494413}
- {fileID: 2147051606546077830}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1.02, y: 1.02, z: 1.02}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3197502779878208215}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2848144350161010536}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4080264242426406278}
- {fileID: 6424283572969246263}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1883476059105349212}
- {fileID: 7811003188552691971}

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: 0.999}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6004833744538984969}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1611213509401803489}
m_Father: {fileID: 4558362294547660329}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3210254045315593125}
- {fileID: 3214500912641965185}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5026269005358103012}
m_Father: {fileID: 8905267601204628791}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1568259630799150366}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1309712272618711075}
m_Father: {fileID: 4558362294547660329}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1.02, y: 1.02, z: 1.02}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4080264242426406278}
m_RootOrder: 0

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6870502395855781705}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2677764702249492100}
m_RootOrder: 1

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3197502779878208215}
- {fileID: 6159542077236467165}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8905267601204628791}
m_Father: {fileID: 0}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3620679801590011701}
m_Father: {fileID: 5026269005358103012}

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: 0.999}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3404880114691434542}
m_Father: {fileID: 3210254045315593125}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1.02, y: 1.02, z: 1.02}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6870502395855781705}
m_RootOrder: 0

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: 0.999}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6205733139118454793}
m_Father: {fileID: 3214500912641965185}

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4080264242426406278}
m_RootOrder: 1

m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 552927569557660881}
- {fileID: 7414980691921613268}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5671183532491584052}
m_RootOrder: 0

384
Assets/Scenes/mainScene.unity


m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!114 &113557733 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6055523672257132380, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: edfa4d53eaae84c43ba581088940700c, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &151543605 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 2812013971948110896, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
m_PrefabInstance: {fileID: 7716713812904700119}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &203192673
GameObject:
m_ObjectHideFlags: 0

m_Father: {fileID: 0}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &297599733 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 8905224144162831968, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &309485569 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 65572184039752926, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &457234252
GameObject:
m_ObjectHideFlags: 0

m_Script: {fileID: 11500000, guid: 78d292f3bd9f1614cb744dcb4fe3ac12, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &883450645 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 3727964864104658339, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &921599257 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6253235256787798393, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}

m_CorrespondingSourceObject: {fileID: 3513144941912357516, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
--- !u!114 &1217229506 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6326316181187829680, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1412109061 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 3143918963127177442, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1439850645 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 35249394896e31643ba0641c2a6f8e8d, type: 3}
--- !u!114 &1511612118 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 8895314912834654425, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1583737884 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6399412716226000571, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1793980663 stripped
--- !u!114 &1818474178 stripped
m_CorrespondingSourceObject: {fileID: 6680128790398423813, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_CorrespondingSourceObject: {fileID: 7029468144569480209, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Script: {fileID: 11500000, guid: d125c6cac111c6442ac5b07a1f313fa4, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &1853015249

m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1853015249}
m_CullTransparentMesh: 1
--- !u!114 &1969944515 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 2646044666659190602, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2003557942 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 1683469940080917438, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}

m_Script: {fileID: 11500000, guid: 78d292f3bd9f1614cb744dcb4fe3ac12, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2074106027 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5162490130543683956, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2126854580 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6900625576974141932, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2130620598 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 4144223224519676544, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1547097153363664359 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 7294450586020064062, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045694 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 1554643161126302255, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: edfa4d53eaae84c43ba581088940700c, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045695 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 4847401366899542036, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045696 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5773825590554619550, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045698 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6606885600234288061, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 788267441}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045699 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 142122066828140637, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {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: 8211845410637438850, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}

value: 0
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_countdown
value:
objectReference: {fileID: 1818474178}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_vivoxUserHandlers.Array.size
value: 4
objectReference: {fileID: 0}

objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalUserObservers.Array.size
value: 7
value: 6
value: 8
value: 6
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_vivoxUserHandlers.Array.data[0]

- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_GameStateObservers.Array.data[0]
value:
objectReference: {fileID: 2637199315837045698}
objectReference: {fileID: 0}
objectReference: {fileID: 1547097153363664359}
objectReference: {fileID: 0}
objectReference: {fileID: 2637199315837045696}
objectReference: {fileID: 0}
objectReference: {fileID: 883450645}
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_GameStateObservers.Array.data[4]
value:

- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LobbyDataObservers.Array.data[0]
value:
objectReference: {fileID: 297599733}
objectReference: {fileID: 0}
objectReference: {fileID: 2126854580}
objectReference: {fileID: 0}
objectReference: {fileID: 2130620598}
objectReference: {fileID: 0}
objectReference: {fileID: 2074106027}
objectReference: {fileID: 0}
objectReference: {fileID: 309485569}
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LobbyDataObservers.Array.data[5]
value:

value:
objectReference: {fileID: 1412109061}
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LobbyDataObservers.Array.data[7]
value:

- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalUserObservers.Array.data[0]
value:
objectReference: {fileID: 2637199315837045695}
objectReference: {fileID: 0}
objectReference: {fileID: 1217229506}
objectReference: {fileID: 0}
objectReference: {fileID: 1969944515}
objectReference: {fileID: 0}
objectReference: {fileID: 1583737884}
objectReference: {fileID: 0}
objectReference: {fileID: 1793980663}
objectReference: {fileID: 0}
objectReference: {fileID: 2637199315837045699}
objectReference: {fileID: 0}
objectReference: {fileID: 151543605}
objectReference: {fileID: 0}
objectReference: {fileID: 1412109061}
objectReference: {fileID: 0}
objectReference: {fileID: 297599733}
objectReference: {fileID: 0}
objectReference: {fileID: 2130620598}
objectReference: {fileID: 0}
objectReference: {fileID: 2074106027}
objectReference: {fileID: 0}
objectReference: {fileID: 309485569}
objectReference: {fileID: 0}
objectReference: {fileID: 2126854580}
objectReference: {fileID: 0}
objectReference: {fileID: 1511612118}
objectReference: {fileID: 0}
objectReference: {fileID: 7716713812904700120}
objectReference: {fileID: 0}
objectReference: {fileID: 1511612118}
objectReference: {fileID: 0}
objectReference: {fileID: 1511612118}
objectReference: {fileID: 0}
objectReference: {fileID: 2637199315837045694}
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalGameStateObservers.Array.size
value: 6

value: 4
value: 0
objectReference: {fileID: 2637199315837045694}
objectReference: {fileID: 0}
objectReference: {fileID: 113557733}
objectReference: {fileID: 0}
objectReference: {fileID: 2637199315837045698}
objectReference: {fileID: 0}
objectReference: {fileID: 1547097153363664359}
objectReference: {fileID: 0}
objectReference: {fileID: 2637199315837045696}
objectReference: {fileID: 0}
objectReference: {fileID: 883450645}
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalGameStateObservers.Array.data[4]
value:

- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalMenuStateObservers.Array.data[0]
value:
objectReference: {fileID: 2637199315837045698}
objectReference: {fileID: 0}
objectReference: {fileID: 1547097153363664359}
objectReference: {fileID: 0}
objectReference: {fileID: 2637199315837045696}
objectReference: {fileID: 0}
objectReference: {fileID: 883450645}
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636911, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_RootOrder
value: 4

objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
--- !u!114 &7716713812904700120 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5235782363599194820, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
m_PrefabInstance: {fileID: 7716713812904700119}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1001 &8628454959146822954
PrefabInstance:
m_ObjectHideFlags: 0

objectReference: {fileID: 0}
- target: {fileID: 1282422528845055840, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
value: 0.0000015571713
value: 0.000030517578
value: 0.0000036023557
objectReference: {fileID: 0}
- target: {fileID: 1386321973193631240, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.x

objectReference: {fileID: 0}
- target: {fileID: 1791708704057470616, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1791708704057470616, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2017918237437651757, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}

objectReference: {fileID: 0}
- target: {fileID: 2832906903618240629, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 155.5
value: 0
value: -27.5
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2938110563166987729, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Target
value:
objectReference: {fileID: 1439850645}
- target: {fileID: 5962742399928014509, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_fontSize
value: 40
objectReference: {fileID: 0}
- target: {fileID: 6062621839658634998, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_fontSize
value: 40
objectReference: {fileID: 0}
- target: {fileID: 6274653283217461060, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 6314503137265266719, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 49.5
value: 0
value: -27.5
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 102.5
value: 0
value: -27.5
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6468178321825623330, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 7029771435095122102, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 208.5
value: 0
value: -27.5
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7182908607506352373, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8628454959292210618, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_fontSize
value: 62
objectReference: {fileID: 0}
- target: {fileID: 8690778207903056580, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
propertyPath: m_AnchorMax.x
value: 0

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedComponents:
- {fileID: 3143918963127177442, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
- {fileID: 6900625576974141932, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_SourcePrefab: {fileID: 100100000, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}

2
Assets/Scripts/GameLobby/Auth/Auth.cs.meta


fileFormatVersion: 2
guid: b98aae4acd443d24faedf0aa20e946f3
guid: 2d4f181652f814a93be872600da7fa0b
MonoImporter:
externalObjects: {}
serializedVersion: 2

49
Assets/Scripts/GameLobby/Game/Countdown.cs


/// since precise timing isn't necessary.
/// </summary>
[RequireComponent(typeof(UI.CountdownUI))]
public class Countdown : MonoBehaviour, IReceiveMessages
public class Countdown : MonoBehaviour
public class Data : Observed<Countdown.Data>
{
private float m_timeLeft;
public float TimeLeft
{
get => m_timeLeft;
set
{ m_timeLeft = value;
OnChanged(this);
}
}
public override void CopyObserved(Data oldObserved) { /*No-op, since this is unnecessary.*/ }
}
CallbackValue<float> TimeLeft = new CallbackValue<float>();
private Data m_data = new Data();
private UI.CountdownUI m_ui;
private const int k_countdownTime = 4;

m_ui = GetComponent<UI.CountdownUI>();
m_data.TimeLeft = -1;
Locator.Get.Messenger.Subscribe(this);
m_ui.BeginObserving(m_data);
TimeLeft.onChanged += m_ui.OnTimeChanged;
TimeLeft.Value = -1;
public void OnDisable()
public void StartCountDown()
Locator.Get.Messenger.Unsubscribe(this);
m_ui.EndObserving();
TimeLeft.Value = k_countdownTime;
public void OnReceiveMessage(MessageType type, object msg)
public void CancelCountDown()
if (type == MessageType.StartCountdown)
{
m_data.TimeLeft = k_countdownTime;
}
else if (type == MessageType.CancelCountdown)
{
m_data.TimeLeft = -1;
}
TimeLeft.Value = -1;
if (m_data.TimeLeft < 0)
if (TimeLeft.Value < 0)
m_data.TimeLeft -= Time.deltaTime;
if (m_data.TimeLeft < 0)
Locator.Get.Messenger.OnReceiveMessage(MessageType.CompleteCountdown, null);
TimeLeft.Value -= Time.deltaTime;
if (TimeLeft.Value < 0)
GameManager.Instance.FinishedCountDown();
}
}

462
Assets/Scripts/GameLobby/Game/GameManager.cs


using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using LobbyRelaySample.ngo;
using Unity.Services.Authentication;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using ParrelSync;
#endif
/// Current state of the local game.
/// Set as a flag to allow for the Inspector to select multiple valid states for various UI features.
/// </summary>
[Flags]
public enum GameState
{
Menu = 1,
Lobby = 2,
JoinMenu = 4,
}
/// <summary>
/// Sets up and runs the entire sample.
/// All the Data that is important gets updated in here, the GameManager in the mainScene has all the references
/// needed to run the game.

#region UI elements that observe the local state. These should be assigned the observers in the scene during Start.
/// <summary>
/// The Observer/Observed Pattern is great for keeping the UI in Sync with the actual Values.
/// Each list below represents a single Observed class that gets updated by other parts of the code, and will
/// trigger the list of Observers that are looking for changes in that class.
///
/// The list is serialized, so you can navigate to the Observers via the Inspector to see who's watching.
/// </summary>
[SerializeField]
private List<LocalMenuStateObserver> m_LocalMenuStateObservers = new List<LocalMenuStateObserver>();
public LocalLobby LocalLobby => m_LocalLobby;
public Action<GameState> onGameStateChanged;
public LocalLobbyList LobbyList { get; private set; } = new LocalLobbyList();
public GameState LocalGameState { get; private set; }
public LobbyManager LobbyManager { get; private set; }
private List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>();
SetupInGame m_setupInGame;
private List<LobbyUserObserver> m_LocalUserObservers = new List<LobbyUserObserver>();
[SerializeField]
private List<LobbyServiceDataObserver> m_LobbyServiceObservers = new List<LobbyServiceDataObserver>();
Countdown m_countdown;
#endregion
private LocalMenuState m_LocalMenuState = new LocalMenuState();
private LobbyUser m_localUser;
private LocalLobby m_localLobby;
private LobbyServiceData m_lobbyServiceData = new LobbyServiceData();
private LobbyContentUpdater m_LobbyContentUpdater = new LobbyContentUpdater();
LocalPlayer m_LocalUser;
LocalLobby m_LocalLobby;
LobbySynchronizer m_LobbySynchronizer;
private RelayUtpSetup m_relaySetup;
private RelayUtpClient m_relayClient;
RelayUtpSetup m_RelaySetup;
RelayUtpClient m_RelayClient;
private vivox.VivoxSetup m_vivoxSetup = new vivox.VivoxSetup();
vivox.VivoxSetup m_VivoxSetup = new vivox.VivoxSetup();
private List<vivox.VivoxUserHandler> m_vivoxUserHandlers;
List<vivox.VivoxUserHandler> m_vivoxUserHandlers;
LobbyColor m_lobbyColorFilter;
static GameManager m_GameManagerInstance;
public static GameManager Instance
{
get
{
if (m_GameManagerInstance != null)
return m_GameManagerInstance;
m_GameManagerInstance = FindObjectOfType<GameManager>();
return m_GameManagerInstance;
}
}
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
public void SetLobbyColorFilter(int color)

private LobbyColor m_lobbyColorFilter;
#region Setup
private void Awake()
public async Task CreateLobby(string name, bool isPrivate, int maxPlayers = 4)
// Do some arbitrary operations to instantiate singletons.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
var unused = Locator.Get;
#pragma warning restore IDE0059
var lobby = await LobbyManager.CreateLobbyAsync(
name,
maxPlayers,
isPrivate, m_LocalUser);
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn));
Application.wantsToQuit += OnWantToQuit;
}
if (lobby != null)
{
try
{
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
private void Start()
{
m_localLobby = new LocalLobby { State = LobbyState.Lobby };
m_localUser = new LobbyUser();
m_localUser.DisplayName = "New Player";
Locator.Get.Messenger.Subscribe(this);
BeginObservers();
}
}
catch(Exception exception)
{
Debug.LogError(exception);
}
private void OnAuthSignIn()
{
Debug.Log("Signed in.");
Debug.Log("Found Lobby");
m_localUser.ID = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
m_localUser.DisplayName = NameGenerator.GetName(m_localUser.ID);
m_localLobby.AddPlayer(m_localUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.
StartVivoxLogin();
}
CreateLobby();
}
else
{
Debug.Log("Didnt find Lobby");
/// <summary>
/// TODO Wire is a good update to remove the monolithic observers and move to observed values instead, on a Singleton gameManager
/// </summary>
private void BeginObservers()
{
foreach (var gameStateObs in m_LocalMenuStateObservers)
gameStateObs.BeginObserving(m_LocalMenuState);
foreach (var serviceObs in m_LobbyServiceObservers)
serviceObs.BeginObserving(m_lobbyServiceData);
foreach (var lobbyObs in m_LocalLobbyObservers)
lobbyObs.BeginObserving(m_localLobby);
foreach (var userObs in m_LocalUserObservers)
userObs.BeginObserving(m_localUser);
SetGameState(GameState.JoinMenu);
}
#endregion
/// <summary>
/// The Messaging System handles most of the core Lobby Service calls, and catches the callbacks from those calls.

/// </summary>
public async void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.CreateLobbyRequest)
if (type == MessageType.JoinLobbyRequest)
LocalLobby.LobbyData createLobbyData = (LocalLobby.LobbyData)msg;
var lobby = await LobbyAsyncRequests.Instance.CreateLobbyAsync(
createLobbyData.LobbyName,
createLobbyData.MaxPlayerCount,
createLobbyData.Private, m_localUser);
LocalLobby lobbyInfo = (LocalLobby)msg;
var lobby = await LobbyManager.JoinLobbyAsync(lobbyInfo.LobbyID.Value, lobbyInfo.LobbyCode.Value,
m_LocalUser);
LobbyConverters.RemoteToLocal(lobby, m_localLobby);
OnCreatedLobby();
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
JoinLobby();
OnFailedJoin();
SetGameState(GameState.JoinMenu);
}
else if (type == MessageType.JoinLobbyRequest)
{
LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg;
var lobby = await LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode,
m_localUser);
if (lobby != null)
{
LobbyConverters.RemoteToLocal(lobby, m_localLobby);
OnJoinedLobby();
}
else
{
OnFailedJoin();
}
m_lobbyServiceData.State = LobbyQueryState.Fetching;
var qr = await LobbyAsyncRequests.Instance.RetrieveLobbyListAsync(m_lobbyColorFilter);
LobbyList.QueryState.Value = LobbyQueryState.Fetching;
var qr = await LobbyManager.RetrieveLobbyListAsync(m_lobbyColorFilter);
OnLobbiesQueried(LobbyConverters.QueryToLocalList(qr));
SetCurrentLobbies(LobbyConverters.QueryToLocalList(qr));
OnLobbyQueryFailed();
LobbyList.QueryState.Value = LobbyQueryState.Error;
var lobby = await LobbyAsyncRequests.Instance.QuickJoinLobbyAsync(m_localUser, m_lobbyColorFilter);
var lobby = await LobbyManager.QuickJoinLobbyAsync(m_LocalUser, m_lobbyColorFilter);
LobbyConverters.RemoteToLocal(lobby, m_localLobby);
OnJoinedLobby();
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
JoinLobby();
OnFailedJoin();
SetGameState(GameState.JoinMenu);
}
}
else if (type == MessageType.RenameRequest)

{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Empty Name not allowed."); // Lobby error type, then HTTP error type.
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Empty Name not allowed."); // Lobby error type, then HTTP error type.
m_localUser.DisplayName = (string)msg;
}
else if (type == MessageType.ClientUserApproved)
{
ConfirmApproval();
m_LocalUser.DisplayName.Value = (string)msg;
m_localUser.Emote = emote;
m_LocalUser.Emote.Value = emote;
m_localUser.UserStatus = (UserStatus)msg;
}
else if (type == MessageType.StartCountdown)
{
m_localLobby.State = LobbyState.CountDown;
}
else if (type == MessageType.CancelCountdown)
{
m_localLobby.State = LobbyState.Lobby;
m_LocalUser.UserStatus.Value = (UserStatus)msg;
if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
//Start game for everyone
if (m_RelayClient is RelayUtpHost)
(m_RelayClient as RelayUtpHost).SendInGameState();
else if (type == MessageType.ConfirmInGameState)
{
m_localUser.UserStatus = UserStatus.InGame;
m_localLobby.State = LobbyState.InGame;
}
else if (type == MessageType.ConfirmInGameState) { }
m_localLobby.State = LobbyState.Lobby;
m_LocalLobby.LocalLobbyState.Value = LobbyState.Lobby;
private void SetGameState(GameState state)
public void BeginCountdown()
bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) && m_LocalMenuState.State == GameState.Lobby;
m_LocalMenuState.State = state;
m_LocalLobby.LocalLobbyState.Value = LobbyState.CountDown;
m_countdown.StartCountDown();
}
public void CancelCountDown()
{
m_countdown.CancelCountDown();
m_LocalLobby.LocalLobbyState.Value = LobbyState.Lobby;
}
public void FinishedCountDown()
{
m_LocalUser.UserStatus.Value = UserStatus.InGame;
m_LocalLobby.LocalLobbyState.Value = LobbyState.InGame;
m_setupInGame.StartNetworkedGame(m_LocalLobby, m_LocalUser);
}
#region Setup
async void Awake()
{
// Do some arbitrary operations to instantiate singletons.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
var unused = Locator.Get;
#pragma warning restore IDE0059
Application.wantsToQuit += OnWantToQuit;
LobbyManager = new LobbyManager();
m_LobbySynchronizer = new LobbySynchronizer(LobbyManager);
m_LocalUser = new LocalPlayer("", false, "LocalPlayer");
m_LocalLobby = new LocalLobby { LocalLobbyState = { Value = LobbyState.Lobby } };
await InitializeServices();
AuthenticatePlayer();
StartVivoxLogin();
Locator.Get.Messenger.Subscribe(this);
}
async Task InitializeServices()
{
string serviceProfileName = "player";
#if UNITY_EDITOR
serviceProfileName = $"{serviceProfileName}_{ClonesManager.GetCurrentProject().name}";
#endif
await Auth.Authenticate(serviceProfileName);
}
void AuthenticatePlayer()
{
var localId = AuthenticationService.Instance.PlayerId;
var randomName = NameGenerator.GetName(localId);
m_LocalUser.ID.Value = localId;
m_LocalUser.DisplayName.Value = randomName;
m_LocalLobby.AddPlayer(m_LocalUser); // The local LocalPlayer object will be hooked into UI
}
public async Task<LocalPlayer> LocalUserInitialized()
{
while (m_LocalUser == null)
await Task.Delay(100);
return m_LocalUser;
}
#endregion
void SetGameState(GameState state)
{
bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) &&
LocalGameState == GameState.Lobby;
LocalGameState = state;
Debug.Log($"Switching Game State to : {LocalGameState}");
OnLeftLobby();
LeaveLobby();
onGameStateChanged.Invoke(LocalGameState);
private void OnLobbiesQueried(IEnumerable<LocalLobby> lobbies)
void SetCurrentLobbies(IEnumerable<LocalLobby> lobbies)
newLobbyDict.Add(lobby.LobbyID, lobby);
newLobbyDict.Add(lobby.LobbyID.Value, lobby);
m_lobbyServiceData.State = LobbyQueryState.Fetched;
m_lobbyServiceData.CurrentLobbies = newLobbyDict;
LobbyList.QueryState.Value = LobbyQueryState.Fetched;
LobbyList.CurrentLobbies = newLobbyDict;
private void OnLobbyQueryFailed()
void CreateLobby()
m_lobbyServiceData.State = LobbyQueryState.Error;
Debug.Log("Creating Lobby");
m_LocalUser.IsHost.Value = true;
JoinLobby();
private void OnCreatedLobby()
void JoinLobby()
m_localUser.IsHost = true;
OnJoinedLobby();
}
Debug.Log("Joining Lobby");
private void OnJoinedLobby()
{
m_LobbyContentUpdater.BeginTracking(m_localLobby, m_localUser);
m_LobbySynchronizer.StartSynch(m_LocalLobby, m_LocalUser);
// The host has the opportunity to reject incoming players, but to do so the player needs to connect to Relay without having game logic available.
// In particular, we should prevent players from joining voice chat until they are approved.
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting);
if (m_localUser.IsHost)
{
StartRelayConnection();
StartVivoxJoin();
}
else
{
StartRelayConnection();
}
StartVivoxJoin();
private async void OnLeftLobby()
void LeaveLobby()
m_localUser.ResetState();
m_LocalUser.ResetState();
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID);
LobbyManager.LeaveLobbyAsync();
m_LobbyContentUpdater.EndTracking();
m_vivoxSetup.LeaveLobbyChannel();
m_LobbySynchronizer.EndSynch();
m_VivoxSetup.LeaveLobbyChannel();
if (m_relaySetup != null)
if (m_RelaySetup != null)
Component.Destroy(m_relaySetup);
m_relaySetup = null;
Component.Destroy(m_RelaySetup);
m_RelaySetup = null;
if (m_relayClient != null)
if (m_RelayClient != null)
m_relayClient.Dispose();
m_RelayClient.Dispose();
StartCoroutine(FinishCleanup());
// We need to delay slightly to give the disconnect message sent during Dispose time to reach the host, so that we don't destroy the connection without it being flushed first.

Component.Destroy(m_relayClient);
m_relayClient = null;
Component.Destroy(m_RelayClient);
m_RelayClient = null;
/// <summary>
/// Back to Join menu if we fail to join for whatever reason.
/// </summary>
private void OnFailedJoin()
{
SetGameState(GameState.JoinMenu);
}
private void StartVivoxLogin()
void StartVivoxLogin()
m_vivoxSetup.Initialize(m_vivoxUserHandlers, OnVivoxLoginComplete);
m_VivoxSetup.Initialize(m_vivoxUserHandlers, OnVivoxLoginComplete);
void OnVivoxLoginComplete(bool didSucceed)
{

StartCoroutine(RetryConnection(StartVivoxLogin, m_localLobby.LobbyID));
StartCoroutine(RetryConnection(StartVivoxLogin, m_LocalLobby.LobbyID.Value));
private void StartVivoxJoin()
void StartVivoxJoin()
m_vivoxSetup.JoinLobbyChannel(m_localLobby.LobbyID, OnVivoxJoinComplete);
m_VivoxSetup.JoinLobbyChannel(m_LocalLobby.LobbyID.Value, OnVivoxJoinComplete);
void OnVivoxJoinComplete(bool didSucceed)
{

StartCoroutine(RetryConnection(StartVivoxJoin, m_localLobby.LobbyID));
StartCoroutine(RetryConnection(StartVivoxJoin, m_LocalLobby.LobbyID.Value));
private void StartRelayConnection()
void StartRelayConnection()
if (m_localUser.IsHost)
m_relaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
if (m_LocalUser.IsHost.Value)
m_RelaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
m_relaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected);
m_RelaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
m_RelaySetup.BeginRelayJoin(m_LocalLobby, m_LocalUser, OnRelayConnected);
Component.Destroy(m_relaySetup);
m_relaySetup = null;
Component.Destroy(m_RelaySetup);
m_RelaySetup = null;
StartCoroutine(RetryConnection(StartRelayConnection, m_localLobby.LobbyID));
StartCoroutine(RetryConnection(StartRelayConnection, m_LocalLobby.LobbyID.Value));
m_relayClient = client;
if (m_localUser.IsHost)
CompleteRelayConnection();
else
Debug.Log("Client is now waiting for approval...");
m_RelayClient = client;
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
private IEnumerator RetryConnection(Action doConnection, string lobbyId)
IEnumerator RetryConnection(Action doConnection, string lobbyId)
if (m_localLobby != null && m_localLobby.LobbyID == lobbyId && !string.IsNullOrEmpty(lobbyId)) // Ensure we didn't leave the lobby during this waiting period.
if (m_LocalLobby != null && m_LocalLobby.LobbyID.Value == lobbyId && !string.IsNullOrEmpty(lobbyId)
) // Ensure we didn't leave the lobby during this waiting period.
private void ConfirmApproval()
void SetUserLobbyState()
if (!m_localUser.IsHost && m_localUser.IsApproved)
{
CompleteRelayConnection();
StartVivoxJoin();
}
}
private void CompleteRelayConnection()
{
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
private void SetUserLobbyState()
{
Debug.Log($"Setting Lobby user state {GameState.Lobby}");
private void ResetLocalLobby()
void ResetLocalLobby()
m_localLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary<string, LobbyUser>());
m_localLobby.AddPlayer(m_localUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
m_localLobby.RelayServer = null;
m_LocalLobby.ResetLobby();
m_LocalLobby
.AddPlayer(m_LocalUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
m_LocalLobby.RelayServer = null;
}
#region Teardown

/// So, we need to delay just briefly to let the request happen (though we don't need to wait for the result).
/// </summary>
private IEnumerator LeaveBeforeQuit()
IEnumerator LeaveBeforeQuit()
{
ForceLeaveAttempt();
yield return null;

private bool OnWantToQuit()
bool OnWantToQuit()
bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID);
bool canQuit = string.IsNullOrEmpty(m_LocalLobby?.LobbyID.Value);
private void OnDestroy()
void OnDestroy()
m_LobbySynchronizer.Dispose();
LobbyManager.Dispose();
private void ForceLeaveAttempt()
void ForceLeaveAttempt()
if (!string.IsNullOrEmpty(m_localLobby?.LobbyID))
if (!string.IsNullOrEmpty(m_LocalLobby?.LobbyID.Value))
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID);
LobbyManager.LeaveLobbyAsync();
m_localLobby = null;
m_LocalLobby = null;
}
}

4
Assets/Scripts/GameLobby/Game/LobbyUserObserver.cs


namespace LobbyRelaySample
{
/// <summary>
/// Holds a LobbyUser value and notifies all subscribers when it has been changed.
/// Holds a LocalPlayer value and notifies all subscribers when it has been changed.
public class LobbyUserObserver : ObserverBehaviour<LobbyUser> { }
public class LobbyUserObserver : ObserverBehaviour<LocalPlayer> { }
}

353
Assets/Scripts/GameLobby/Game/LocalLobby.cs


[System.Serializable]
public class LocalLobby : Observed<LocalLobby>
{
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>();
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers;
public bool canPullUpdate;
public bool CanSetChanged = true;
public Action<Dictionary<string, LocalPlayer>> onUserListChanged;
Dictionary<string, LocalPlayer> m_LocalPlayers = new Dictionary<string, LocalPlayer>();
public Dictionary<string, LocalPlayer> LocalPlayers => m_LocalPlayers;
public struct LobbyData
ServerAddress m_RelayServer;
public LocalLobby()
public string LobbyID { get; set; }
public string LobbyCode { get; set; }
public string RelayCode { get; set; }
public string RelayNGOCode { get; set; }
public string LobbyName { get; set; }
public bool Private { get; set; }
public bool Locked { get; set; }
public int AvailableSlots { get; set; }
public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public LobbyColor Color { get; set; }
public long State_LastEdit { get; set; }
public long Color_LastEdit { get; set; }
public long RelayNGOCode_LastEdit { get; set; }
LastUpdated.Value = DateTime.Now.ToFileTimeUtc();
LobbyID.onChanged = (s) => { SetValueChanged(); };
LobbyCode.onChanged = (s) => { SetValueChanged(); };
RelayCode.onChanged = (s) => { SetValueChanged(); };
RelayNGOCode.onChanged = (s) => { SetValueChanged(); };
RelayServer.onChanged = (s) => { SetValueChanged(); };
LobbyName.onChanged = (s) => { SetValueChanged(); };
LocalLobbyState.onChanged = (s) => { SetValueChanged(); };
Private.onChanged = (s) => { SetValueChanged(); };
AvailableSlots.onChanged = (s) => { SetValueChanged(); };
MaxPlayerCount.onChanged = (s) => { SetValueChanged(); };
LocalLobbyColor.onChanged = (s) => { SetValueChanged(); };
LastUpdated.onChanged = (s) => { SetValueChanged(); };
}
/// <summary>Used only for visual output of the Relay connection info. The obfuscated Relay server IP is obtained during allocation in the RelayUtpSetup.</summary>
#endregion
public LobbyData(LobbyData existing)
{
LobbyID = existing.LobbyID;
LobbyCode = existing.LobbyCode;
RelayCode = existing.RelayCode;
RelayNGOCode = existing.RelayNGOCode;
LobbyName = existing.LobbyName;
Private = existing.Private;
MaxPlayerCount = existing.MaxPlayerCount;
State = existing.State;
Color = existing.Color;
State_LastEdit = existing.State_LastEdit;
Color_LastEdit = existing.Color_LastEdit;
RelayNGOCode_LastEdit = existing.RelayNGOCode_LastEdit;
AvailableSlots = existing.AvailableSlots;
Locked = existing.Locked;
}
public CallbackValue<string> LobbyID = new CallbackValue<string>();
public LobbyData(string lobbyCode)
{
LobbyID = null;
LobbyCode = lobbyCode;
RelayCode = null;
RelayNGOCode = null;
LobbyName = null;
Private = false;
MaxPlayerCount = -1;
State = LobbyState.Lobby;
Color = LobbyColor.None;
State_LastEdit = 0;
Color_LastEdit = 0;
RelayNGOCode_LastEdit = 0;
AvailableSlots = 4;
Locked = false;
}
public CallbackValue<string> LobbyCode = new CallbackValue<string>();
public override string ToString()
{
StringBuilder sb = new StringBuilder("Lobby : ");
sb.AppendLine(LobbyName);
sb.Append("ID: ");
sb.AppendLine(LobbyID);
sb.Append("Code: ");
sb.AppendLine(LobbyCode);
sb.Append("Private: ");
sb.AppendLine(Private.ToString());
sb.Append("Locked: ");
sb.AppendLine(Locked.ToString());
sb.Append("Max Players: ");
sb.AppendLine(MaxPlayerCount.ToString());
sb.Append("AvailableSlots: ");
sb.AppendLine(AvailableSlots.ToString());
sb.Append("LobbyState: ");
sb.AppendLine(State.ToString());
sb.Append("Lobby State Last Edit: ");
sb.AppendLine(new DateTime(State_LastEdit).ToString());
sb.Append("LobbyColor: ");
sb.AppendLine(Color.ToString());
sb.Append("Color Last Edit: ");
sb.AppendLine(new DateTime(Color_LastEdit).ToString());
sb.Append("RelayCode: ");
sb.AppendLine(RelayCode);
sb.Append("RelayNGO: ");
sb.AppendLine(RelayNGOCode);
sb.Append("Relay NGO last Edit: ");
sb.AppendLine(new DateTime(RelayNGOCode_LastEdit).ToString());
return sb.ToString();
}
}
public CallbackValue<string> RelayCode = new CallbackValue<string>();
public LobbyData Data => m_Data;
LobbyData m_Data;
public CallbackValue<string> RelayNGOCode = new CallbackValue<string>();
ServerAddress m_RelayServer;
public CallbackValue<ServerAddress> RelayServer = new CallbackValue<ServerAddress>();
/// <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;
set
{
m_RelayServer = value;
OnChanged(this);
}
}
public CallbackValue<string> LobbyName = new CallbackValue<string>();
#endregion
public CallbackValue<LobbyState> LocalLobbyState = new CallbackValue<LobbyState>();
public void AddPlayer(LobbyUser user)
{
if (m_LobbyUsers.ContainsKey(user.ID))
{
Debug.LogError($"Cant add player {user.DisplayName}({user.ID}) to lobby: {LobbyID} twice");
return;
}
public CallbackValue<bool> Private = new CallbackValue<bool>();
DoAddPlayer(user);
OnChanged(this);
}
public CallbackValue<int> AvailableSlots = new CallbackValue<int>();
private void DoAddPlayer(LobbyUser user)
{
m_LobbyUsers.Add(user.ID, user);
user.onChanged += OnChangedUser;
}
public CallbackValue<int> MaxPlayerCount = new CallbackValue<int>();
public void RemovePlayer(LobbyUser user)
{
DoRemoveUser(user);
OnChanged(this);
}
public CallbackValue<LobbyColor> LocalLobbyColor = new CallbackValue<LobbyColor>();
private void DoRemoveUser(LobbyUser user)
{
if (!m_LobbyUsers.ContainsKey(user.ID))
{
Debug.LogWarning($"Player {user.DisplayName}({user.ID}) does not exist in lobby: {LobbyID}");
return;
}
public CallbackValue<long> LastUpdated = new CallbackValue<long>();
m_LobbyUsers.Remove(user.ID);
user.onChanged -= OnChangedUser;
}
public int PlayerCount => m_LocalPlayers.Count;
private void OnChangedUser(LobbyUser user)
public void ResetLobby()
OnChanged(this);
}
m_LocalPlayers.Clear();
public string LobbyID
{
get => m_Data.LobbyID;
set
{
m_Data.LobbyID = value;
OnChanged(this);
}
}
LobbyName.Value = "";
public string LobbyCode
{
get => m_Data.LobbyCode;
set
{
m_Data.LobbyCode = value;
OnChanged(this);
}
LobbyID.Value = "";
LobbyCode.Value = "";
Private.Value = false;
LocalLobbyColor.Value = LobbyRelaySample.LobbyColor.None;
AvailableSlots.Value = 4;
MaxPlayerCount.Value = 4;
public string RelayCode
/// <summary>
/// A locking mechanism for registering when something has looked at the Lobby to see if anything has changed
/// </summary>
/// <returns></returns>
public bool IsLobbyChanged()
get => m_Data.RelayCode;
set
{
m_Data.RelayCode = value;
OnChanged(this);
}
bool isChanged = m_ValuesChanged;
m_ValuesChanged = false;
return isChanged;
public string RelayNGOCode
void SetValueChanged()
get => m_Data.RelayNGOCode;
set
{
m_Data.RelayNGOCode = value;
m_Data.RelayNGOCode_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
if(CanSetChanged)
m_ValuesChanged = true;
bool m_ValuesChanged;
public string LobbyName
public void AddPlayer(LocalPlayer user)
get => m_Data.LobbyName;
set
if (m_LocalPlayers.ContainsKey(user.ID.Value))
m_Data.LobbyName = value;
OnChanged(this);
Debug.LogError($"Cant add player {user.DisplayName}({user.ID}) to lobby: {LobbyID} twice");
return;
}
public LobbyState State
{
get => m_Data.State;
set
{
m_Data.State = value;
m_Data.State_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
Debug.Log($"Adding User: {user.DisplayName} - {user.ID}");
m_LocalPlayers.Add(user.ID.Value, user);
onUserListChanged?.Invoke(m_LocalPlayers);
public bool Private
public void RemovePlayer(LocalPlayer user)
get => m_Data.Private;
set
{
m_Data.Private = value;
OnChanged(this);
}
DoRemoveUser(user);
onUserListChanged?.Invoke(m_LocalPlayers);
public int PlayerCount => m_LobbyUsers.Count;
public int MaxPlayerCount
void DoRemoveUser(LocalPlayer user)
get => m_Data.MaxPlayerCount;
set
if (!m_LocalPlayers.ContainsKey(user.ID.Value))
m_Data.MaxPlayerCount = value;
OnChanged(this);
Debug.LogWarning($"Player {user.DisplayName}({user.ID}) does not exist in lobby: {LobbyID}");
return;
}
public LobbyColor Color
{
get => m_Data.Color;
set
{
if (m_Data.Color != value)
{
m_Data.Color = value;
m_Data.Color_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
}
m_LocalPlayers.Remove(user.ID.Value);
public void CopyObserved(LobbyData data, Dictionary<string, LobbyUser> currUsers)
public override string ToString()
// It's possible for the host to edit the lobby in between the time they last pushed lobby data and the time their pull for new lobby data completes.
// If that happens, the edit will be lost, so instead we maintain the time of last edit to detect that case.
var pendingState = data.State;
var pendingColor = data.Color;
var pendingNgoCode = data.RelayNGOCode;
if (m_Data.State_LastEdit > data.State_LastEdit)
pendingState = m_Data.State;
if (m_Data.Color_LastEdit > data.Color_LastEdit)
pendingColor = m_Data.Color;
if (m_Data.RelayNGOCode_LastEdit > data.RelayNGOCode_LastEdit)
pendingNgoCode = m_Data.RelayNGOCode;
m_Data = data;
m_Data.State = pendingState;
m_Data.Color = pendingColor;
m_Data.RelayNGOCode = pendingNgoCode;
if (currUsers == null)
m_LobbyUsers = new Dictionary<string, LobbyUser>();
else
{
List<LobbyUser> toRemove = new List<LobbyUser>();
foreach (var oldUser in m_LobbyUsers)
{
if (currUsers.ContainsKey(oldUser.Key))
oldUser.Value.CopyObserved(currUsers[oldUser.Key]);
else
toRemove.Add(oldUser.Value);
}
foreach (var remove in toRemove)
{
DoRemoveUser(remove);
}
foreach (var currUser in currUsers)
{
if (!m_LobbyUsers.ContainsKey(currUser.Key))
DoAddPlayer(currUser.Value);
}
}
StringBuilder sb = new StringBuilder("Lobby : ");
sb.AppendLine(LobbyName.Value);
sb.Append("ID: ");
sb.AppendLine(LobbyID.Value);
sb.Append("Code: ");
sb.AppendLine(LobbyCode.Value);
sb.Append("Private: ");
sb.AppendLine(Private.Value.ToString());
sb.Append("AvailableSlots: ");
sb.AppendLine(AvailableSlots.Value.ToString());
sb.Append("Max Players: ");
sb.AppendLine(MaxPlayerCount.Value.ToString());
sb.Append("LocalLobbyState: ");
sb.AppendLine(LocalLobbyState.Value.ToString());
sb.Append("Lobby LocalLobbyState Last Edit: ");
sb.AppendLine(new DateTime(LastUpdated.Value).ToString());
sb.Append("LocalLobbyColor: ");
sb.AppendLine(LocalLobbyColor.Value.ToString());
sb.Append("RelayCode: ");
sb.AppendLine(RelayCode.Value);
sb.Append("RelayNGO: ");
sb.AppendLine(RelayNGOCode.Value);
OnChanged(this);
return sb.ToString();
CopyObserved(oldObserved.Data, oldObserved.m_LobbyUsers);
// CopyObserved(oldObserved.Data, oldObserved.m_LocalPlayers);
}
}

22
Assets/Scripts/GameLobby/Game/ServerAddress.cs


using System;
public class ServerAddress
public class ServerAddress : IEquatable<ServerAddress>
{
string m_IP;
int m_Port;

{
return $"{m_IP}:{m_Port}";
}
public bool Equals(ServerAddress other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return m_IP == other.m_IP && m_Port == other.m_Port;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ServerAddress)obj);
}
}
}

7
Assets/Scripts/GameLobby/Infrastructure/Locator.cs


using LobbyRelaySample.Auth;
using System;
using System.Collections.Generic;

{
Provide(new Messenger());
Provide(new UpdateSlowNoop());
Provide(new IdentityNoop());
Provide(new ngo.InGameInputHandlerNoop());
FinishConstruction();

public IUpdateSlow UpdateSlow => Locate<IUpdateSlow>();
public void Provide(IUpdateSlow updateSlow) { ProvideAny(updateSlow); }
public IIdentity Identity => Locate<IIdentity>();
public void Provide(IIdentity identity) { ProvideAny(identity); }
}
}

2
Assets/Scripts/GameLobby/Infrastructure/Messenger.cs


/// </summary>
public class Messenger : IMessenger
{
private List<IReceiveMessages> m_receivers = new List<IReceiveMessages>();
List<IReceiveMessages> m_receivers = new List<IReceiveMessages>();
private const float k_durationToleranceMs = 15;
// We need to handle subscribers who modify the receiver list, e.g. a subscriber who unsubscribes in their OnReceiveMessage.

4
Assets/Scripts/GameLobby/Infrastructure/Observed.cs


/// Something that exposes some data that, when changed, an observer would want to be notified about automatically.
/// Used for UI elements and for keeping our local Lobby state synchronized with the remote Lobby service data.
/// (See http://gameprogrammingpatterns.com/observer.html to learn more.)
///
///
/// In your Observed child implementations, be sure to call OnChanged when setting the value of any property.
/// </summary>
/// <typeparam name="T">The type of object to be observed.</typeparam>

public Action<T> onDestroyed { get; set; }
/// <summary>
/// Should be implemented into every public property of the observed
/// Should be implemented into every public property of the observed
/// </summary>
/// <param name="observed">Instance of the observed that changed.</param>
protected void OnChanged(T observed)

2
Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs


Debug.LogError($"Needs a Target of type {typeof(T)} to begin observing.", gameObject);
return;
}
UpdateObserver(target);
observed.onChanged += UpdateObserver;
}

14
Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs


/// </summary>
public class UpdateSlow : MonoBehaviour, IUpdateSlow
{
private class Subscriber
class Subscriber
public Subscriber(UpdateMethod updateMethod, float period)
{
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;
float m_durationToleranceMs = 10;
private bool m_doNotRemoveIfTooLong = false;
private List<Subscriber> m_subscribers = new List<Subscriber>();
bool m_doNotRemoveIfTooLong = false;
List<Subscriber> m_subscribers = new List<Subscriber>();
public void Awake()
{

}
private void Update()
void Update()
{
OnUpdate(Time.deltaTime);
}

148
Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs


/// </summary>
public static class LobbyConverters
{
const string key_RelayCode = nameof(LocalLobby.RelayCode);
const string key_RelayNGOCode = nameof(LocalLobby.RelayNGOCode);
const string key_LobbyState = nameof(LocalLobby.LocalLobbyState);
const string key_LobbyColor = nameof(LocalLobby.LocalLobbyColor);
const string key_LastEdit = nameof(LocalLobby.LastUpdated);
const string key_Displayname = nameof(LocalPlayer.DisplayName);
const string key_Userstatus = nameof(LocalPlayer.UserStatus);
const string key_Emote = nameof(LocalPlayer.Emote);
data.Add("RelayCode", lobby.RelayCode);
data.Add("RelayNGOCode", lobby.RelayNGOCode);
data.Add("State", ((int)lobby.State).ToString()); // Using an int is smaller than using the enum state's name.
data.Add("Color", ((int)lobby.Color).ToString());
data.Add("State_LastEdit", lobby.Data.State_LastEdit.ToString());
data.Add("Color_LastEdit", lobby.Data.Color_LastEdit.ToString());
data.Add("RelayNGOCode_LastEdit", lobby.Data.RelayNGOCode_LastEdit.ToString());
data.Add(key_RelayCode, lobby.RelayCode.Value);
data.Add(key_RelayNGOCode, lobby.RelayNGOCode.Value);
data.Add(key_LobbyState,
((int)lobby.LocalLobbyState.Value)
.ToString()); // Using an int is smaller than using the enum state's name.
data.Add(key_LobbyColor, ((int)lobby.LocalLobbyColor.Value).ToString());
data.Add(key_LastEdit, lobby.LastUpdated.Value.ToString());
public static Dictionary<string, string> LocalToRemoteUserData(LobbyUser user)
public static Dictionary<string, string> LocalToRemoteUserData(LocalPlayer user)
if (user == null || string.IsNullOrEmpty(user.ID))
if (user == null || string.IsNullOrEmpty(user.ID.Value))
data.Add("DisplayName", user.DisplayName); // The lobby doesn't need to know any data beyond the name and state; Relay will handle the rest.
data.Add("UserStatus", ((int)user.UserStatus).ToString());
data.Add(key_Displayname, user.DisplayName.Value);
data.Add(key_Userstatus,
((int)user.UserStatus.Value)
.ToString()); // Cheaper to send the string int of the enum over the string enum
data.Add(key_Emote, (user.Emote).ToString());
return data;
}

public static void RemoteToLocal(Lobby lobby, LocalLobby lobbyToUpdate)
public static void RemoteToLocal(Lobby remoteLobby, LocalLobby localLobby, bool allowSetLobbyChanged = true)
//Copy Data from Lobby into Local lobby fields
LocalLobby.LobbyData info = new LocalLobby.LobbyData(lobbyToUpdate.Data)
if (remoteLobby == null)
LobbyID = lobby.Id,
LobbyCode = lobby.LobbyCode,
Private = lobby.IsPrivate,
LobbyName = lobby.Name,
MaxPlayerCount = lobby.MaxPlayers,
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : lobbyToUpdate.RelayCode, // By providing RelayCode through the lobby data with Member visibility, we ensure a client is connected to the lobby before they could attempt a relay connection, preventing timing issues between them.
RelayNGOCode = lobby.Data?.ContainsKey("RelayNGOCode") == true ? lobby.Data["RelayNGOCode"].Value : lobbyToUpdate.RelayNGOCode,
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState)int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby,
Color = lobby.Data?.ContainsKey("Color") == true ? (LobbyColor)int.Parse(lobby.Data["Color"].Value) : LobbyColor.None,
State_LastEdit = lobby.Data?.ContainsKey("State_LastEdit") == true ? long.Parse(lobby.Data["State_LastEdit"].Value) : lobbyToUpdate.Data.State_LastEdit,
Color_LastEdit = lobby.Data?.ContainsKey("Color_LastEdit") == true ? long.Parse(lobby.Data["Color_LastEdit"].Value) : lobbyToUpdate.Data.Color_LastEdit,
RelayNGOCode_LastEdit = lobby.Data?.ContainsKey("RelayNGOCode_LastEdit") == true ? long.Parse(lobby.Data["RelayNGOCode_LastEdit"].Value) : lobbyToUpdate.Data.RelayNGOCode_LastEdit
};
Debug.LogError("Remote lobby is null, cannot convert.");
return;
}
if (localLobby == null)
{
Debug.LogError("Local Lobby is null, cannot convert");
return;
}
localLobby.CanSetChanged = allowSetLobbyChanged;
localLobby.LobbyID.Value = remoteLobby.Id;
localLobby.LobbyName.Value = remoteLobby.Name;
localLobby.LobbyCode.Value = remoteLobby.LobbyCode;
localLobby.Private.Value = remoteLobby.IsPrivate;
localLobby.AvailableSlots.Value = remoteLobby.AvailableSlots;
localLobby.MaxPlayerCount.Value = remoteLobby.MaxPlayers;
localLobby.LastUpdated.Value = remoteLobby.LastUpdated.ToFileTimeUtc();
Dictionary<string, LobbyUser> lobbyUsers = new Dictionary<string, LobbyUser>();
foreach (var player in lobby.Players)
//Custom Data Conversion
localLobby.RelayCode.Value = remoteLobby.Data?.ContainsKey(key_RelayCode) == true
? remoteLobby.Data[key_RelayCode].Value
: localLobby.RelayCode.Value;
localLobby.RelayNGOCode.Value = remoteLobby.Data?.ContainsKey(key_RelayNGOCode) == true
? remoteLobby.Data[key_RelayNGOCode].Value
: localLobby.RelayNGOCode.Value;
localLobby.LocalLobbyState.Value = remoteLobby.Data?.ContainsKey(key_LobbyState) == true
? (LobbyState)int.Parse(remoteLobby.Data[key_LobbyState].Value)
: LobbyState.Lobby;
localLobby.LocalLobbyColor.Value = remoteLobby.Data?.ContainsKey(key_LobbyColor) == true
? (LobbyColor)int.Parse(remoteLobby.Data[key_LobbyColor].Value)
: LobbyColor.None;
List<string> remotePlayerIDs = new List<string>();
foreach (var player in remoteLobby.Players)
// If we already know about this player and this player is already connected to Relay, don't overwrite things that Relay might be changing.
if (player.Data?.ContainsKey("UserStatus") == true && int.TryParse(player.Data["UserStatus"].Value, out int status))
var id = player.Id;
remotePlayerIDs.Add(id);
var isHost = remoteLobby.HostId.Equals(player.Id);
var displayName = player.Data?.ContainsKey(key_Displayname) == true
? player.Data[key_Displayname].Value
: default;
var emote = player.Data?.ContainsKey(key_Emote) == true
? (EmoteType)int.Parse(player.Data[key_Emote].Value)
: default;
var userStatus = player.Data?.ContainsKey(key_Userstatus) == true
? (UserStatus)int.Parse(player.Data[key_Userstatus].Value)
: UserStatus.Connecting;
LocalPlayer localPlayer;
//See if we have the remote player locally already
if (localLobby.LocalPlayers.ContainsKey(player.Id))
if (status > (int)UserStatus.Connecting && lobbyToUpdate.LobbyUsers.ContainsKey(player.Id))
{
lobbyUsers.Add(player.Id, lobbyToUpdate.LobbyUsers[player.Id]);
continue;
}
localPlayer = localLobby.LocalPlayers[player.Id];
localPlayer.ID.Value = id;
localPlayer.DisplayName.Value = displayName;
localPlayer.Emote.Value = emote;
localPlayer.UserStatus.Value = userStatus;
}
else
{
localPlayer = new LocalPlayer(id, isHost, displayName, emote, userStatus);
localLobby.AddPlayer(localPlayer);
}
// If the player isn't connected to Relay, get the most recent data that the lobby knows.
// (If we haven't seen this player yet, a new local representation of the player will have already been added by the LocalLobby.)
LobbyUser incomingData = new LobbyUser
{
IsHost = lobby.HostId.Equals(player.Id),
DisplayName = player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : default,
Emote = player.Data?.ContainsKey("Emote") == true ? (EmoteType)int.Parse(player.Data["Emote"].Value) : default,
UserStatus = player.Data?.ContainsKey("UserStatus") == true ? (UserStatus)int.Parse(player.Data["UserStatus"].Value) : UserStatus.Connecting,
ID = player.Id
};
lobbyUsers.Add(incomingData.ID, incomingData);
var disconnectedUsers = new List<LocalPlayer>();
foreach (var (id, player) in localLobby.LocalPlayers)
{
if (!remotePlayerIDs.Contains(id))
disconnectedUsers.Add(player);
}
foreach (var remove in disconnectedUsers)
{
localLobby.RemovePlayer(remove);
//Push all the data at once so we don't call OnChanged for each variable
lobbyToUpdate.CopyObserved(info, lobbyUsers);
localLobby.CanSetChanged = true;
}
/// <summary>

return retLst;
}
private static LocalLobby RemoteToNewLocal(Lobby lobby)
static LocalLobby RemoteToNewLocal(Lobby lobby)
{
LocalLobby data = new LocalLobby();
RemoteToLocal(lobby, data);

8
Assets/Scripts/GameLobby/LobbyRelaySample.asmdef


"GUID:f2d49d9fa7e7eb3418e39723a7d3b92f",
"GUID:6087a74f6015aae4daed9a2577a7596c",
"GUID:4ebbcb26024b547159a86c39de1a8fa5",
"GUID:3bf5041814073ec4089849c425919d5a",
"GUID:3b8ed52f1b5c64994af4c4e0aa4b6c4b"
"GUID:3b8ed52f1b5c64994af4c4e0aa4b6c4b",
"GUID:894a6cc6ed5cd2645bb542978cbed6a9"
"precompiledReferences": [],
"precompiledReferences": [
""
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],

6
Assets/Scripts/GameLobby/NGO/InGameRunner.cs


/// </summary>
public class InGameRunner : NetworkBehaviour, IInGameInputHandler
{
private Action m_onConnectionVerified, m_onGameEnd;
Action m_onConnectionVerified, m_onGameEnd;
private int m_expectedPlayerCount; // Used by the host, but we can't call the RPC until the network connection completes.
private bool? m_canSpawnInGameObjects;
private Queue<Vector2> m_pendingSymbolPositions = new Queue<Vector2>();

private Transform m_symbolContainerInstance;
private PlayerData m_localUserData; // This has an ID that's not necessarily the OwnerClientId, since all clients will see all spawned objects regardless of ownership.
public void Initialize(Action onConnectionVerified, int expectedPlayerCount, Action onGameEnd, LobbyUser localUser)
public void Initialize(Action onConnectionVerified, int expectedPlayerCount, Action onGameEnd, LocalPlayer localUser)
m_localUserData = new PlayerData(localUser.DisplayName, 0);
m_localUserData = new PlayerData(localUser.DisplayName.Value, 0);
Locator.Get.Provide(this); // Simplifies access since some networked objects can't easily communicate locally (e.g. the host might call a ClientRpc without that client knowing where the call originated).
}

4
Assets/Scripts/GameLobby/NGO/IntroOutroRunner.cs


/// </summary>
public class IntroOutroRunner : MonoBehaviour
{
[SerializeField] private Animator m_animator;
private Action m_onOutroComplete;
[SerializeField] Animator m_animator;
Action m_onOutroComplete;
public void DoIntro()
{

14
Assets/Scripts/GameLobby/NGO/NetworkedDataStore.cs


// Using a singleton here since we need spawned PlayerCursors to be able to find it, but we don't need the flexibility offered by the Locator.
public static NetworkedDataStore Instance;
private Dictionary<ulong, PlayerData> m_playerData = new Dictionary<ulong, PlayerData>();
private ulong m_localId;
Dictionary<ulong, PlayerData> m_playerData = new Dictionary<ulong, PlayerData>();
ulong m_localId;
private Action<PlayerData> m_onGetCurrentCallback;
private UnityEvent<PlayerData> m_onEachPlayerCallback;
Action<PlayerData> m_onGetCurrentCallback;
UnityEvent<PlayerData> m_onEachPlayerCallback;
public void Awake()
{

}
[ServerRpc(RequireOwnership = false)]
private void GetAllPlayerData_ServerRpc(ulong callerId)
void GetAllPlayerData_ServerRpc(ulong callerId)
{
var sortedData = m_playerData.Select(kvp => kvp.Value).OrderByDescending(data => data.score);
GetAllPlayerData_ClientRpc(callerId, sortedData.ToArray());

private void GetAllPlayerData_ClientRpc(ulong callerId, PlayerData[] sortedData)
void GetAllPlayerData_ClientRpc(ulong callerId, PlayerData[] sortedData)
{
if (callerId != m_localId)
return;

}
[ServerRpc(RequireOwnership = false)]
private void GetPlayerData_ServerRpc(ulong id, ulong callerId)
void GetPlayerData_ServerRpc(ulong id, ulong callerId)
{
if (m_playerData.ContainsKey(id))
GetPlayerData_ClientRpc(callerId, m_playerData[id]);

4
Assets/Scripts/GameLobby/NGO/PlayerCursor.cs


[RequireComponent(typeof(Collider))]
public class PlayerCursor : NetworkBehaviour, IReceiveMessages
{
[SerializeField] private SpriteRenderer m_renderer = default;
[SerializeField] private ParticleSystem m_onClickParticles = default;
[SerializeField] SpriteRenderer m_renderer = default;
[SerializeField] ParticleSystem m_onClickParticles = default;
[SerializeField] private TMPro.TMP_Text m_nameOutput = default;
private Camera m_mainCamera;
private NetworkVariable<Vector3> m_position = new NetworkVariable<Vector3>( Vector3.zero); // (Using a NetworkTransform to sync position would also work.)

8
Assets/Scripts/GameLobby/NGO/ResultsUserUI.cs


public class ResultsUserUI : NetworkBehaviour
{
[Tooltip("The containers for the player data outputs, in order, to be hidden until the game ends.")]
[SerializeField] private CanvasGroup[] m_containers;
[SerializeField] CanvasGroup[] m_containers;
[SerializeField] private TMPro.TMP_Text[] m_playerNameOutputs;
[SerializeField] TMPro.TMP_Text[] m_playerNameOutputs;
[SerializeField] private TMPro.TMP_Text[] m_playerScoreOutputs;
private int m_index = 0;
[SerializeField] TMPro.TMP_Text[] m_playerScoreOutputs;
int m_index = 0;
public void Start()
{

10
Assets/Scripts/GameLobby/NGO/Scorer.cs


/// </summary>
public class Scorer : NetworkBehaviour
{
[SerializeField] private NetworkedDataStore m_dataStore = default;
private ulong m_localId;
[SerializeField] private TMP_Text m_scoreOutputText = default;
[SerializeField] NetworkedDataStore m_dataStore = default;
ulong m_localId;
[SerializeField] TMP_Text m_scoreOutputText = default;
[SerializeField] private UnityEvent<PlayerData> m_onGameEnd = default;
[SerializeField] UnityEvent<PlayerData> m_onGameEnd = default;
public override void OnNetworkSpawn()
{

}
[ClientRpc]
private void UpdateScoreOutput_ClientRpc(ulong id, int score)
void UpdateScoreOutput_ClientRpc(ulong id, int score)
{
if (m_localId == id)
m_scoreOutputText.text = score.ToString("00");

111
Assets/Scripts/GameLobby/NGO/SetupInGame.cs


using System;
using System.Threading.Tasks;
using LobbyRelaySample.relay;
using Unity.Services.Relay;
using UnityEngine.SocialPlatforms;
/// Once the local player is in a lobby and that lobby has entered the In-Game state, this will load in whatever is necessary to actually run the game part.
/// Once the local localPlayer is in a localLobby and that localLobby has entered the In-Game state, this will load in whatever is necessary to actually run the game part.
[SerializeField] private GameObject m_IngameRunnerPrefab = default;
[SerializeField] private GameObject[] m_disableWhileInGame = default;
[SerializeField]
GameObject m_IngameRunnerPrefab = default;
[SerializeField]
private GameObject[] m_disableWhileInGame = default;
private InGameRunner m_inGameRunner;

private Action<UnityTransport> m_initializeTransport;
private LobbyUser m_localUser;
{ Locator.Get.Messenger.Subscribe(this);
{
Locator.Get.Messenger.Subscribe(this);
{ Locator.Get.Messenger.Unsubscribe(this);
{
Locator.Get.Messenger.Unsubscribe(this);
}
private void SetMenuVisibility(bool areVisible)

/// The prefab with the NetworkManager contains all of the assets and logic needed to set up the NGO minigame.
/// The UnityTransport needs to also be set up with a new Allocation from Relay.
/// </summary>
private async Task CreateNetworkManager()
async Task CreateNetworkManager(LocalLobby localLobby, LocalPlayer localPlayer)
m_lobby = localLobby;
m_inGameRunner = Instantiate(m_IngameRunnerPrefab).GetComponentInChildren<InGameRunner>();
m_inGameRunner.Initialize(OnConnectionVerified, m_lobby.PlayerCount, OnGameEnd, localPlayer);
if (localPlayer.IsHost.Value)
{
await SetRelayHostData();
NetworkManager.Singleton.StartHost();
}
else
{
await SetRelayClientData();
NetworkManager.Singleton.StartClient();
}
}
async Task SetRelayHostData()
{
if (m_localUser.IsHost)
NetworkManager.Singleton.gameObject.AddComponent<RelayUtpNGOSetupHost>().Initialize(this, m_lobby, () => { m_initializeTransport(transport); NetworkManager.Singleton.StartHost(); });
else
NetworkManager.Singleton.gameObject.AddComponent<RelayUtpNGOSetupClient>().Initialize(this, m_lobby, () => { m_initializeTransport(transport); NetworkManager.Singleton.StartClient(); });
await Task.Delay(1);
m_inGameRunner = Instantiate(m_IngameRunnerPrefab).GetComponentInChildren<InGameRunner>();
m_inGameRunner.Initialize(OnConnectionVerified, m_lobby.PlayerCount, OnGameEnd, m_localUser);
var allocation = await Relay.Instance.CreateAllocationAsync(m_lobby.MaxPlayerCount.Value);
var joincode = await Relay.Instance.GetJoinCodeAsync(allocation.AllocationId);
m_lobby.RelayNGOCode.Value = joincode;
bool isSecure = false;
var endpoint = RelayUtpSetup.GetEndpointForAllocation(allocation.ServerEndpoints,
allocation.RelayServer.IpV4, allocation.RelayServer.Port, out isSecure);
transport.SetHostRelayData(RelayUtpSetup.AddressFromEndpoint(endpoint), endpoint.Port,
allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, isSecure);
private void OnConnectionVerified()
{ m_hasConnectedViaNGO = true;
}
async Task SetRelayClientData()
{
UnityTransport transport = NetworkManager.Singleton.GetComponentInChildren<UnityTransport>();
// These are public for use in the Inspector.
public void OnLobbyChange(LocalLobby lobby)
{ m_lobby = lobby; // Most of the time this is redundant, but we need to get multiple members of the lobby to the Relay setup components, so might as well just hold onto the whole thing.
var joinAllocation = await Relay.Instance.JoinAllocationAsync(m_lobby.RelayCode.Value);
bool isSecure = false;
var endpoint = RelayUtpSetup.GetEndpointForAllocation(joinAllocation.ServerEndpoints,
joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, out isSecure);
transport.SetClientRelayData(RelayUtpSetup.AddressFromEndpoint(endpoint), endpoint.Port,
joinAllocation.AllocationIdBytes, joinAllocation.Key,
joinAllocation.ConnectionData, joinAllocation.HostConnectionData, isSecure);
public void OnLocalUserChange(LobbyUser user)
{ m_localUser = user; // Same, regarding redundancy.
private void OnConnectionVerified()
{
m_hasConnectedViaNGO = true;
/// <summary>
/// Once the Relay Allocation is created, this passes its data to the UnityTransport.
/// </summary>
public void SetRelayServerData(string address, int port, byte[] allocationBytes, byte[] key, byte[] connectionData, byte[] hostConnectionData, bool isSecure)
public void StartNetworkedGame(LocalLobby localLobby, LocalPlayer localPlayer)
m_initializeTransport = (transport) => { transport.SetRelayServerData(address, (ushort)port, allocationBytes, key, connectionData, hostConnectionData, isSecure); };
m_doesNeedCleanup = true;
SetMenuVisibility(false);
#pragma warning disable 4014
CreateNetworkManager(localLobby, localPlayer);
#pragma warning restore 4014
if (type == MessageType.ConfirmInGameState)
{
m_doesNeedCleanup = true;
SetMenuVisibility(false);
CreateNetworkManager();
}
if (type == MessageType.ConfirmInGameState) { }
// If this player hasn't successfully connected via NGO, forcibly exit the minigame.
// If this localPlayer hasn't successfully connected via NGO, forcibly exit the minigame.
// Once we're in-game, any state change reflects the player leaving the game, so we should clean up.
// Once we're in-game, any state change reflects the localPlayer leaving the game, so we should clean up.
/// Return to the lobby after the game, whether due to the game ending or due to a failed connection.
/// Return to the localLobby after the game, whether due to the game ending or due to a failed connection.
/// </summary>
private void OnGameEnd()
{

GameObject.Destroy(m_inGameRunner.gameObject); // Since this destroys the NetworkManager, that will kick off cleaning up networked objects.
Destroy(m_inGameRunner
.gameObject); // Since this destroys the NetworkManager, that will kick off cleaning up networked objects.
SetMenuVisibility(true);
m_lobby.RelayNGOCode = null;
m_doesNeedCleanup = false;

}
}

10
Assets/Scripts/GameLobby/NGO/SymbolContainer.cs


public class SymbolContainer : NetworkBehaviour, IReceiveMessages
{
[SerializeField]
private float m_speed = 1;
private bool m_isConnected = false;
private bool m_hasGameStarted = false;
float m_speed = 1;
bool m_isConnected = false;
bool m_hasGameStarted = false;
private void OnGameStarted()
void OnGameStarted()
{
m_hasGameStarted = true;
if (m_isConnected)

BeginMotion();
}
private void BeginMotion()
void BeginMotion()
{
transform.position += Time.deltaTime * m_speed*Vector3.down;
}

4
Assets/Scripts/GameLobby/NGO/SymbolKillVolume.cs


[RequireComponent(typeof(Collider))]
public class SymbolKillVolume : MonoBehaviour
{
private bool m_isInitialized = false;
private Action m_onSymbolCollided;
bool m_isInitialized = false;
Action m_onSymbolCollided;
public void Initialize(Action onSymbolCollided)
{

4
Assets/Scripts/GameLobby/NGO/SymbolObject.cs


/// </summary>
public class SymbolObject : NetworkBehaviour
{
[SerializeField] private SymbolData m_symbolData;
[SerializeField] private SpriteRenderer m_renderer;
[SerializeField] SymbolData m_symbolData;
[SerializeField] SpriteRenderer m_renderer;
[SerializeField] private Animator m_animator;
public bool Clicked { get; private set; }

2
Assets/Scripts/GameLobby/Relay/AsyncRequestRelay.cs


{
public class AsyncRequestRelay : AsyncRequest
{
private static AsyncRequestRelay s_instance;
static AsyncRequestRelay s_instance;
public static AsyncRequestRelay Instance
{
get

2
Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs


/// </summary>
public class RelayPendingApproval : IDisposable
{
private NetworkConnection m_pendingConnection;
NetworkConnection m_pendingConnection;
private bool m_hasDisposed = false;
private const float k_waitTime = 0.1f;
private Action<NetworkConnection, Approval> m_onResult;

50
Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs


/// </summary>
public class RelayUtpClient : MonoBehaviour, IDisposable // This is a MonoBehaviour merely to have access to Update.
{
protected LobbyUser m_localUser;
protected LocalPlayer m_localUser;
protected LocalLobby m_localLobby;
protected NetworkDriver m_networkDriver;
protected List<NetworkConnection> m_connections; // For clients, this has just one member, but for hosts it will have more.

private const float k_heartbeatPeriod = 5;
private bool m_hasDisposed = false;
const float k_heartbeatPeriod = 5;
bool m_hasDisposed = false;
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby)
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LocalPlayer localUser, LocalLobby localLobby)
{
m_localUser = localUser;
m_localLobby = localLobby;

Dispose();
}
private void OnLocalChange(LobbyUser localUser)
private void OnLocalChange(LocalPlayer localUser)
{
if (m_connections.Count == 0) // This could be the case for the host alone in the lobby.
return;

if (msgType == MsgType.PlayerApprovalState)
{
Approval approval = (Approval)msgContents[0];
if (approval == Approval.OK && !m_localUser.IsApproved)
OnApproved(m_networkDriver, conn);
else if (approval == Approval.GameAlreadyStarted)
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Rejected: Game has already started.");
// if (approval == Approval.OK && !m_localUser.IsApproved)
// OnApproved(m_networkDriver, conn);
// else if (approval == Approval.GameAlreadyStarted)
// Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Rejected: Game has already started.");
m_localLobby.LobbyUsers[id].DisplayName = name;
m_localLobby.LocalPlayers[id].DisplayName.Value = name;
m_localLobby.LobbyUsers[id].Emote = emote;
m_localLobby.LocalPlayers[id].Emote.Value = emote;
m_localLobby.LobbyUsers[id].UserStatus = status;
m_localLobby.LocalPlayers[id].UserStatus.Value = status;
}
else if (msgType == MsgType.StartCountdown)
Locator.Get.Messenger.OnReceiveMessage(MessageType.StartCountdown, null);

{
// Don't react to our own messages. Also, don't need to hold onto messages if the ID is absent; clients should be initialized and in the lobby before they send events.
// (Note that this enforces lobby membership before processing any events besides an approval request, so a client is unable to fully use Relay unless they're in the lobby.)
return id != m_localUser.ID && (m_localUser.IsApproved && m_localLobby.LobbyUsers.ContainsKey(id) || type == MsgType.PlayerApprovalState);
return true;// != m_localUser.ID && (m_localUser.IsApproved && m_localLobby.LocalPlayers.ContainsKey(id) || type == MsgType.PlayerApprovalState);
}
protected virtual void ProcessNetworkEventDataAdditional(NetworkConnection conn, MsgType msgType, string id) { }
protected virtual void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)

/// </summary>
private void SendInitialMessage(NetworkDriver driver, NetworkConnection connection)
{
WriteByte(driver, connection, m_localUser.ID, MsgType.NewPlayer, 0);
WriteByte(driver, connection, m_localUser.ID.Value, MsgType.NewPlayer, 0);
m_localUser.IsApproved = true;
// m_localUser.IsApproved = true;
ForceFullUserUpdate(driver, connection, m_localUser);
}

private void DoUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user)
private void DoUserUpdate(NetworkDriver driver, NetworkConnection connection, LocalPlayer user)
// Only update with actual changes. (If multiple change at once, we send messages for each separately, but that shouldn't happen often.)
if (0 < (user.LastChanged & LobbyUser.UserMembers.DisplayName))
WriteString(driver, connection, user.ID, MsgType.PlayerName, user.DisplayName);
if (0 < (user.LastChanged & LobbyUser.UserMembers.Emote))
WriteByte(driver, connection, user.ID, MsgType.Emote, (byte)user.Emote);
if (0 < (user.LastChanged & LobbyUser.UserMembers.UserStatus))
WriteByte(driver, connection, user.ID, MsgType.ReadyState, (byte)user.UserStatus);
protected void ForceFullUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user)
protected void ForceFullUserUpdate(NetworkDriver driver, NetworkConnection connection, LocalPlayer user)
WriteString(driver, connection, user.ID, MsgType.PlayerName, user.DisplayName);
WriteByte(driver, connection, user.ID, MsgType.Emote, (byte)user.Emote);
WriteByte(driver, connection, user.ID, MsgType.ReadyState, (byte)user.UserStatus);
WriteString(driver, connection, user.ID.Value, MsgType.PlayerName, user.DisplayName.Value);
WriteByte(driver, connection, user.ID.Value, MsgType.Emote, (byte)user.Emote.Value);
WriteByte(driver, connection, user.ID.Value, MsgType.ReadyState, (byte)user.UserStatus.Value);
}
/// <summary>

{
foreach (NetworkConnection connection in m_connections)
// If the client calls Disconnect, the host might not become aware right away (depending on when the PubSub messages get pumped), so send a message over UTP instead.
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.PlayerDisconnect, 0);
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.PlayerDisconnect, 0);
m_localLobby.RelayServer = null;
}
}

81
Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs


/// <summary>
/// In addition to maintaining a heartbeat with the Relay server to keep it from timing out, the host player must pass network events
/// from clients to all other clients, since they don't connect to each other.
/// If you are using the Unity Networking Package, you can use their Relay instead of building your own packets.
/// If you are using the Unity Networking Package, you can use their Relay instead of building your own packets.
public override void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby)
public override void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections,
LocalPlayer localUser, LocalLobby localLobby)
m_hasSentInitialMessage = true; // The host will be alone in the lobby at first, so they need not send any messages right away.
m_hasSentInitialMessage =
true; // The host will be alone in the lobby at first, so they need not send any messages right away.
protected override void Uninitialize()
{
base.Uninitialize();

protected override void OnUpdate()
{
if (!m_IsRelayConnected) // If Relay was disconnected somehow, stop taking actions that will keep the allocation alive.
if (!m_IsRelayConnected
) // If Relay was disconnected somehow, stop taking actions that will keep the allocation alive.
return;
base.OnUpdate();
UpdateConnections();

/// If so, they need to be updated with the current state of everyone else.
/// If not, they should be informed and rejected.
/// </summary>
private void OnNewConnection(NetworkConnection conn, string id)
void OnNewConnection(NetworkConnection conn, string id)
private void NewConnectionApprovalResult(NetworkConnection conn, Approval result)
void NewConnectionApprovalResult(NetworkConnection conn, Approval result)
WriteByte(m_networkDriver, conn, m_localUser.ID, MsgType.PlayerApprovalState, (byte)result);
WriteByte(m_networkDriver, conn, m_localUser.ID.Value, MsgType.PlayerApprovalState, (byte)result);
foreach (var user in m_localLobby.LobbyUsers)
foreach (var user in m_localLobby.LocalPlayers)
ForceFullUserUpdate(m_networkDriver, conn, user.Value);
m_connections.Add(conn);
}

protected override bool CanProcessDataEventFor(NetworkConnection conn, MsgType type, string id)
{
// Don't send through data from one client to everyone else if they haven't been approved yet. (They should also not be sending data if not approved, so this is a backup.)
return id != m_localUser.ID && (m_localLobby.LobbyUsers.ContainsKey(id) && m_connections.Contains(conn) || type == MsgType.NewPlayer);
return id != m_localUser.ID.Value &&
(m_localLobby.LocalPlayers.ContainsKey(id) && m_connections.Contains(conn) ||
type == MsgType.NewPlayer);
}
protected override void ProcessNetworkEventDataAdditional(NetworkConnection conn, MsgType msgType, string id)

{
string name = m_localLobby.LobbyUsers[id].DisplayName;
string name = m_localLobby.LocalPlayers[id].DisplayName.Value;
foreach (NetworkConnection otherConn in m_connections)
{
if (otherConn == conn)

}
else if (msgType == MsgType.Emote || msgType == MsgType.ReadyState)
{
byte value = msgType == MsgType.Emote ? (byte)m_localLobby.LobbyUsers[id].Emote : (byte)m_localLobby.LobbyUsers[id].UserStatus;
byte value = msgType == MsgType.Emote
? (byte)m_localLobby.LocalPlayers[id].Emote.Value
: (byte)m_localLobby.LocalPlayers[id].UserStatus.Value;
foreach (NetworkConnection otherConn in m_connections)
{
if (otherConn == conn)

}
else if (msgType == MsgType.NewPlayer)
OnNewConnection(conn, id);
else if (msgType == MsgType.PlayerDisconnect) // Clients message the host when they intend to disconnect, or else the host ends up keeping the connection open.
else if (msgType == MsgType.PlayerDisconnect
) // Clients message the host when they intend to disconnect, or else the host ends up keeping the connection open.
var queryCooldownMilliseconds = LobbyAsyncRequests.Instance.GetRateLimit(LobbyAsyncRequests.RequestType.Query)
/*var queryCooldownMilliseconds = LobbyManager.Instance.GetRateLimit(LobbyManager.RequestType.Query)
WaitAndCheckUsers(queryCooldownMilliseconds*2);
WaitAndCheckUsers(queryCooldownMilliseconds*2);*/
#pragma warning restore 4014
return;
}

{
if (type == MessageType.LobbyUserStatus)
CheckIfAllUsersReady();
else if (type == MessageType.EndGame) // This assumes that only the host will have the End Game button available; otherwise, clients need to be able to send this message, too.
else if (type == MessageType.EndGame
) // This assumes that only the host will have the End Game button available; otherwise, clients need to be able to send this message, too.
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.EndInGame, 0);
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.EndInGame, 0);
private void CheckIfAllUsersReady()
//TODO Move this to Host Check. Host pushes Start Signal to all users?
void CheckIfAllUsersReady()
foreach (var user in m_localLobby.LobbyUsers)
foreach (var user in m_localLobby.LocalPlayers)
if (user.Value.UserStatus != UserStatus.Ready)
{ haveAllReadied = false;
if (user.Value.UserStatus.Value != UserStatus.Ready)
{
haveAllReadied = false;
if (haveAllReadied && m_localLobby.State == LobbyState.Lobby) // Need to notify both this client and all others that all players have readied.
if (haveAllReadied && m_localLobby.LocalLobbyState.Value == LobbyState.Lobby
) // Need to notify both this client and all others that all players have readied.
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.StartCountdown, 0);
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.StartCountdown, 0);
else if (!haveAllReadied && m_localLobby.State == LobbyState.CountDown) // Someone cancelled during the countdown, so abort the countdown.
else if (!haveAllReadied && m_localLobby.LocalLobbyState.Value == LobbyState.CountDown
) // Someone cancelled during the countdown, so abort the countdown.
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.CancelCountdown, 0);
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.CancelCountdown, 0);
}
}

{
Locator.Get.Messenger.OnReceiveMessage(MessageType.ConfirmInGameState, null);
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.ConfirmInGame, 0);
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.ConfirmInGame, 0);
private void UpdateConnections()
void UpdateConnections()
{
for (int c = m_connections.Count - 1; c >= 0; c--)
{

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.
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.
// Although the connection is created (i.e. Accepted), we still need to approve it, which will trigger when receiving the NewPlayer message from that client.
}
}

foreach (NetworkConnection connection in m_connections)
connection.Disconnect(m_networkDriver); // Note that Lobby won't receive the disconnect immediately, so its auto-disconnect takes 30-40s, if needed.
connection.Disconnect(
m_networkDriver); // Note that Lobby won't receive the disconnect immediately, so its auto-disconnect takes 30-40s, if needed.
}
}

468
Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs


namespace LobbyRelaySample.relay
{
/// <summary>
/// Responsible for setting up a connection with Relay using Unity Transport (UTP). A Relay Allocation is created by the host, and then all players
/// bind UTP to that Allocation in order to send data to each other.
/// Must be a MonoBehaviour since the binding process doesn't have asynchronous callback options.
/// </summary>
public abstract class RelayUtpSetup : MonoBehaviour
{
protected bool m_isRelayConnected = false;
protected NetworkDriver m_networkDriver;
protected List<NetworkConnection> m_connections;
protected NetworkEndPoint m_endpointForServer;
protected LocalLobby m_localLobby;
protected LobbyUser m_localUser;
protected Action<bool, RelayUtpClient> m_onJoinComplete;
/// <summary>
/// Responsible for setting up a connection with Relay using Unity Transport (UTP). A Relay Allocation is created by the host, and then all players
/// bind UTP to that Allocation in order to send data to each other.
/// Must be a MonoBehaviour since the binding process doesn't have asynchronous callback options.
/// </summary>
public abstract class RelayUtpSetup : MonoBehaviour
{
protected bool m_isRelayConnected = false;
protected NetworkDriver m_networkDriver;
protected List<NetworkConnection> m_connections;
protected NetworkEndPoint m_endpointForServer;
protected LocalLobby m_localLobby;
protected LocalPlayer m_localUser;
protected Action<bool, RelayUtpClient> m_onJoinComplete;
public static string AddressFromEndpoint(NetworkEndPoint endpoint)
{
return endpoint.Address.Split(':')[0];
}
public static string AddressFromEndpoint(NetworkEndPoint endpoint)
{
return endpoint.Address.Split(':')[0];
}
public void BeginRelayJoin(LocalLobby localLobby, LobbyUser localUser, Action<bool, RelayUtpClient> onJoinComplete)
{
m_localLobby = localLobby;
m_localUser = localUser;
m_onJoinComplete = onJoinComplete;
JoinRelay();
}
protected abstract void JoinRelay();
public void BeginRelayJoin(
LocalLobby localLobby,
LocalPlayer localUser,
Action<bool, RelayUtpClient> onJoinComplete)
{
m_localLobby = localLobby;
m_localUser = localUser;
m_onJoinComplete = onJoinComplete;
JoinRelay();
}
protected abstract void JoinRelay();
/// <summary>
/// Determine the server endpoint for connecting to the Relay server, for either an Allocation or a JoinAllocation.
/// If DTLS encryption is available, and there's a secure server endpoint available, use that as a secure connection. Otherwise, just connect to the Relay IP unsecured.
/// </summary>
public static NetworkEndPoint GetEndpointForAllocation(List<RelayServerEndpoint> endpoints, string ip, int port, out bool isSecure)
{
#if ENABLE_MANAGED_UNITYTLS
foreach (RelayServerEndpoint endpoint in endpoints)
{
if (endpoint.Secure && endpoint.Network == RelayServerEndpoint.NetworkOptions.Udp)
{
isSecure = true;
return NetworkEndPoint.Parse(endpoint.Host, (ushort)endpoint.Port);
}
}
#endif
isSecure = false;
return NetworkEndPoint.Parse(ip, (ushort)port);
}
/// <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(NetworkEndPoint serverEndpoint, byte[] allocationIdBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes, byte[] hmacKeyBytes, int connectionCapacity, bool isSecure)
{
RelayAllocationId allocationId = ConvertAllocationIdBytes(allocationIdBytes);
RelayConnectionData connectionData = ConvertConnectionDataBytes(connectionDataBytes);
RelayConnectionData hostConnectionData = ConvertConnectionDataBytes(hostConnectionDataBytes);
RelayHMACKey key = ConvertHMACKeyBytes(hmacKeyBytes);
/// <summary>
/// Determine the server endpoint for connecting to the Relay server, for either an Allocation or a JoinAllocation.
/// If DTLS encryption is available, and there's a secure server endpoint available, use that as a secure connection. Otherwise, just connect to the Relay IP unsecured.
/// </summary>
public static NetworkEndPoint GetEndpointForAllocation(
List<RelayServerEndpoint> endpoints,
string ip,
int port,
out bool isSecure)
{
#if ENABLE_MANAGED_UNITYTLS
foreach (RelayServerEndpoint endpoint in endpoints)
{
if (endpoint.Secure && endpoint.Network == RelayServerEndpoint.NetworkOptions.Udp)
{
isSecure = true;
return NetworkEndPoint.Parse(endpoint.Host, (ushort) endpoint.Port);
}
}
#endif
isSecure = false;
return NetworkEndPoint.Parse(ip, (ushort) port);
}
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref hostConnectionData, ref key, isSecure);
relayServerData.ComputeNewNonce(); // For security, the nonce value sent when authenticating the allocation must be increased.
var networkSettings = new NetworkSettings();
/// <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(
NetworkEndPoint serverEndpoint,
byte[] allocationIdBytes,
byte[] connectionDataBytes,
byte[] hostConnectionDataBytes,
byte[] hmacKeyBytes,
int connectionCapacity,
bool isSecure)
{
RelayAllocationId allocationId = ConvertAllocationIdBytes(allocationIdBytes);
RelayConnectionData connectionData = ConvertConnectionDataBytes(connectionDataBytes);
RelayConnectionData hostConnectionData = ConvertConnectionDataBytes(hostConnectionDataBytes);
RelayHMACKey key = ConvertHMACKeyBytes(hmacKeyBytes);
m_networkDriver = NetworkDriver.Create(networkSettings.WithRelayParameters(ref relayServerData));
m_connections = new List<NetworkConnection>(connectionCapacity);
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData,
ref hostConnectionData, ref key, isSecure);
relayServerData
.ComputeNewNonce(); // For security, the nonce value sent when authenticating the allocation must be increased.
var networkSettings = new NetworkSettings();
if (m_networkDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
Debug.LogError("Failed to bind to Relay allocation.");
else
StartCoroutine(WaitForBindComplete());
}
m_networkDriver = NetworkDriver.Create(networkSettings.WithRelayParameters(ref relayServerData));
m_connections = new List<NetworkConnection>(connectionCapacity);
private IEnumerator WaitForBindComplete()
{
while (!m_networkDriver.Bound)
{
m_networkDriver.ScheduleUpdate().Complete();
yield return null;
}
OnBindingComplete();
}
if (m_networkDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
Debug.LogError("Failed to bind to Relay allocation.");
else
StartCoroutine(WaitForBindComplete());
}
protected abstract void OnBindingComplete();
private IEnumerator WaitForBindComplete()
{
while (!m_networkDriver.Bound)
{
m_networkDriver.ScheduleUpdate().Complete();
yield return null;
}
#region UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them.
unsafe private static RelayAllocationId ConvertAllocationIdBytes(byte[] allocationIdBytes)
{
fixed (byte* ptr = allocationIdBytes)
{
return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length);
}
}
OnBindingComplete();
}
unsafe private static RelayConnectionData ConvertConnectionDataBytes(byte[] connectionData)
{
fixed (byte* ptr = connectionData)
{
return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length);
}
}
protected abstract void OnBindingComplete();
unsafe private static RelayHMACKey ConvertHMACKeyBytes(byte[] hmac)
{
fixed (byte* ptr = hmac)
{
return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length);
}
}
#endregion
#region UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them.
private void OnDestroy()
{
if (!m_isRelayConnected && m_networkDriver.IsCreated)
m_networkDriver.Dispose();
}
}
unsafe private static RelayAllocationId ConvertAllocationIdBytes(byte[] allocationIdBytes)
{
fixed (byte* ptr = allocationIdBytes)
{
return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length);
}
}
/// <summary>
/// Host logic: Request a new Allocation, and then both bind to it and request a join code. Once those are both complete, supply data back to the lobby.
/// </summary>
public class RelayUtpSetupHost : RelayUtpSetup
{
[Flags]
private enum JoinState { None = 0, Bound = 1, Joined = 2 }
private JoinState m_joinState = JoinState.None;
private Allocation m_allocation;
unsafe private static RelayConnectionData ConvertConnectionDataBytes(byte[] connectionData)
{
fixed (byte* ptr = connectionData)
{
return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length);
}
}
protected override void JoinRelay()
{
RelayAPIInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnAllocation);
}
unsafe private static RelayHMACKey ConvertHMACKeyBytes(byte[] hmac)
{
fixed (byte* ptr = hmac)
{
return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length);
}
}
private void OnAllocation(Allocation allocation)
{
m_allocation = allocation;
RelayAPIInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode);
bool isSecure = false;
m_endpointForServer = GetEndpointForAllocation(allocation.ServerEndpoints, allocation.RelayServer.IpV4, allocation.RelayServer.Port, out isSecure);
BindToAllocation(m_endpointForServer, allocation.AllocationIdBytes, allocation.ConnectionData, allocation.ConnectionData, allocation.Key, 16, isSecure);
}
#endregion
private void OnDestroy()
{
if (!m_isRelayConnected && m_networkDriver.IsCreated)
m_networkDriver.Dispose();
}
}
/// <summary>
/// Host logic: Request a new Allocation, and then both bind to it and request a join code. Once those are both complete, supply data back to the lobby.
/// </summary>
public class RelayUtpSetupHost : RelayUtpSetup
{
[Flags]
private enum JoinState
{
None = 0,
Bound = 1,
Joined = 2
}
private JoinState m_joinState = JoinState.None;
private Allocation m_allocation;
protected override void JoinRelay()
{
RelayAPIInterface.AllocateAsync(m_localLobby.MaxPlayerCount.Value, OnAllocation);
}
private void OnAllocation(Allocation allocation)
{
m_allocation = allocation;
RelayAPIInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode);
bool isSecure = false;
m_endpointForServer = GetEndpointForAllocation(allocation.ServerEndpoints, allocation.RelayServer.IpV4,
allocation.RelayServer.Port, out isSecure);
BindToAllocation(m_endpointForServer, allocation.AllocationIdBytes, allocation.ConnectionData,
allocation.ConnectionData, allocation.Key, 16, isSecure);
}
private void OnRelayCode(string relayCode)
{
m_localLobby.RelayCode = relayCode;
m_localLobby.RelayServer = new ServerAddress(AddressFromEndpoint(m_endpointForServer), m_endpointForServer.Port);
m_joinState |= JoinState.Joined;
private void OnRelayCode(string relayCode)
{
m_localLobby.RelayCode.Value = relayCode;
m_localLobby.RelayServer.Value =
new ServerAddress(AddressFromEndpoint(m_endpointForServer), m_endpointForServer.Port);
m_joinState |= JoinState.Joined;
CheckForComplete();
CheckForComplete();
}
}
protected override void OnBindingComplete()
{
if (m_networkDriver.Listen() != 0)
{
Debug.LogError("RelayUtpSetupHost failed to bind to the Relay Allocation.");
m_onJoinComplete(false, null);
}
else
{
Debug.Log("Relay host is bound.");
m_joinState |= JoinState.Bound;
protected override void OnBindingComplete()
{
if (m_networkDriver.Listen() != 0)
{
Debug.LogError("RelayUtpSetupHost failed to bind to the Relay Allocation.");
m_onJoinComplete(false, null);
}
else
{
Debug.Log("Relay host is bound.");
m_joinState |= JoinState.Bound;
CheckForComplete();
CheckForComplete();
}
}
}
}
private async Task CheckForComplete()
{
if (m_joinState == (JoinState.Joined | JoinState.Bound) && this != null) // this will equal null (i.e. this component has been destroyed) if the host left the lobby during the Relay connection sequence.
{
m_isRelayConnected = true;
RelayUtpHost host = gameObject.AddComponent<RelayUtpHost>();
host.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, host);
await LobbyAsyncRequests.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode);
}
}
}
private void CheckForComplete()
{
if (m_joinState == (JoinState.Joined | JoinState.Bound) && this != null
) // this will equal null (i.e. this component has been destroyed) if the host left the lobby during the Relay connection sequence.
{
m_isRelayConnected = true;
RelayUtpHost host = gameObject.AddComponent<RelayUtpHost>();
host.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, host);
var connectionInfo = $"{m_allocation.RelayServer.IpV4}:{m_allocation.RelayServer.Port}";
// await LobbyManager.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode, connectionInfo);
}
}
}
/// <summary>
/// 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 JoinAllocation m_allocation;
/// <summary>
/// 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 JoinAllocation m_allocation;
protected override void JoinRelay()
{
m_localLobby.onChanged += OnLobbyChange;
}
protected override void JoinRelay()
{
m_localLobby.onChanged += OnLobbyChange;
}
private void OnLobbyChange(LocalLobby lobby)
{
if (m_localLobby.RelayCode != null)
{
RelayAPIInterface.JoinAsync(m_localLobby.RelayCode, OnJoin);
m_localLobby.onChanged -= OnLobbyChange;
}
}
private void OnLobbyChange(LocalLobby lobby)
{
if (m_localLobby.RelayCode != null)
{
RelayAPIInterface.JoinAsync(m_localLobby.RelayCode.Value, OnJoin);
m_localLobby.onChanged -= OnLobbyChange;
}
}
private void OnJoin(JoinAllocation joinAllocation)
{
if (joinAllocation == null || this == null) // The returned JoinAllocation is null if allocation failed. this would be destroyed already if you quit the lobby while Relay is connecting.
return;
m_allocation = joinAllocation;
bool isSecure = false;
m_endpointForServer = GetEndpointForAllocation(joinAllocation.ServerEndpoints, joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, out isSecure);
BindToAllocation(m_endpointForServer, joinAllocation.AllocationIdBytes, joinAllocation.ConnectionData, joinAllocation.HostConnectionData, joinAllocation.Key, 1, isSecure);
m_localLobby.RelayServer = new ServerAddress(AddressFromEndpoint(m_endpointForServer), m_endpointForServer.Port);
}
private void OnJoin(JoinAllocation joinAllocation)
{
if (joinAllocation == null || this == null
) // The returned JoinAllocation is null if allocation failed. this would be destroyed already if you quit the lobby while Relay is connecting.
return;
m_allocation = joinAllocation;
bool isSecure = false;
m_endpointForServer = GetEndpointForAllocation(joinAllocation.ServerEndpoints,
joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, out isSecure);
BindToAllocation(m_endpointForServer, joinAllocation.AllocationIdBytes, joinAllocation.ConnectionData,
joinAllocation.HostConnectionData, joinAllocation.Key, 1, isSecure);
m_localLobby.RelayServer.Value =
new ServerAddress(AddressFromEndpoint(m_endpointForServer), m_endpointForServer.Port);
}
protected override void OnBindingComplete()
{
protected override void OnBindingComplete()
{
ConnectToServer();
ConnectToServer();
}
}
private async Task ConnectToServer()
{
// Once the client is bound to the Relay server, send a connection request.
m_connections.Add(m_networkDriver.Connect(m_endpointForServer));
while (m_networkDriver.GetConnectionState(m_connections[0]) == NetworkConnection.State.Connecting)
{
m_networkDriver.ScheduleUpdate().Complete();
await Task.Delay(100);
}
if (m_networkDriver.GetConnectionState(m_connections[0]) != NetworkConnection.State.Connected)
{
Debug.LogError("RelayUtpSetupClient could not connect to the host.");
m_onJoinComplete(false, null);
}
else if (this != null)
{
m_isRelayConnected = true;
RelayUtpClient client = gameObject.AddComponent<RelayUtpClient>();
client.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, client);
await LobbyAsyncRequests.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode);
}
}
}
private async Task ConnectToServer()
{
// Once the client is bound to the Relay server, send a connection request.
m_connections.Add(m_networkDriver.Connect(m_endpointForServer));
while (m_networkDriver.GetConnectionState(m_connections[0]) == NetworkConnection.State.Connecting)
{
m_networkDriver.ScheduleUpdate().Complete();
await Task.Delay(100);
}
if (m_networkDriver.GetConnectionState(m_connections[0]) != NetworkConnection.State.Connected)
{
Debug.LogError("RelayUtpSetupClient could not connect to the host.");
m_onJoinComplete(false, null);
}
else if (this != null)
{
m_isRelayConnected = true;
RelayUtpClient client = gameObject.AddComponent<RelayUtpClient>();
client.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, client);
var connectionInfo = $"{m_allocation.RelayServer.IpV4}:{m_allocation.RelayServer.Port}";
// await LobbyManager.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode,connectionInfo);
}
}
}
}

8
Assets/Scripts/GameLobby/Tests/Editor/MessengerTests.cs


{
#region Test classes
/// <summary>Trivial message recipient that will run some action on any message.</summary>
private class Subscriber : IReceiveMessages
class Subscriber : IReceiveMessages
private Action m_thingToDo;
Action m_thingToDo;
public Subscriber(Action thingToDo) { m_thingToDo = thingToDo; }
public void OnReceiveMessage(MessageType type, object msg) { m_thingToDo?.Invoke(); }
}

{
Messenger messenger = new Messenger();
int msgCount = 0;
SubscriberArgs sub = new SubscriberArgs((type, msg) => {
SubscriberArgs sub = new SubscriberArgs((type, msg) => {
msgCount++; // These are just for simple detection of the intended behavior.
if (type == MessageType.RenameRequest) msgCount += 9;
if (msg is string) msgCount += int.Parse(msg as string);

Assert.AreEqual(1, msgCount, "Should have acted on the message.");
}
}
}
}

4
Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs


}
// We just have a couple Observers that update some arbitrary member, in this case a string.
private class TestObserved : Observed<TestObserved>
class TestObserved : Observed<TestObserved>
{
string m_stringField;

}
}
private class TestObserverBehaviour : ObserverBehaviour<TestObserved>
class TestObserverBehaviour : ObserverBehaviour<TestObserved>
{
public string displayStringField;

148
Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs


using NUnit.Framework;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Debug = UnityEngine.Debug;
namespace Test
{

/// </summary>
public class LobbyRoundtripTests
{
private string m_workingLobbyId;
private LobbyRelaySample.Auth.SubIdentity_Authentication m_auth;
private bool m_didSigninComplete = false;
private Dictionary<string, PlayerDataObject> m_mockUserData; // This is handled in the LobbyAsyncRequest calls normally, but we need to supply this for the direct Lobby API calls.
LobbyUser m_LocalUser;
string playerID;
Dictionary<string, PlayerDataObject>
m_mockUserData; // This is handled in the LobbyAsyncRequest calls normally, but we need to supply this for the direct Lobby API calls.
LocalPlayer m_LocalUser;
LobbyManager m_LobbyManager;
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; });
m_mockUserData.Add("DisplayName", new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "TestUser123"));
m_LocalUser = new LobbyUser(true);
m_mockUserData.Add("DisplayName",
new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "TestUser123"));
#pragma warning disable 4014
TestAuthSetup();
#pragma warning restore 4014
m_LocalUser = new LocalPlayer(Auth.ID(), false, "TESTPLAYER");
m_LobbyManager = new LobbyManager();
[UnityTearDown]
public IEnumerator PerTestTeardown()
async Task TestAuthSetup()
if (m_workingLobbyId != null)
{ yield return AsyncTestHelper.Await(async ()=> await LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_workingLobbyId));
m_workingLobbyId = null;
}
yield return new WaitForSeconds(0.5f); // We need a yield anyway, so wait long enough to probably delete the lobby. There currently (6/22/2021) aren't other tests that would have issues if this took longer.
await Auth.Authenticate("test");
[OneTimeTearDown]
public void Teardown()
[UnityTearDown]
public IEnumerator PerTestTeardown()
m_auth?.Dispose();
if (m_LobbyManager.CurrentLobby != null)
{
yield return AsyncTestHelper.Await(async () => await m_LobbyManager.LeaveLobbyAsync());
}
}
/// <summary>

{
#region Setup
// Wait a reasonable amount of time for sign-in to complete.
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
// Since we're signed in through the same pathway as the actual game, the list of lobbies will include any that have been made in the game itself, so we should account for those.
// If you want to get around this, consider having a secondary project using the same assets with its own credentials.

Debug.Log("Getting Lobby List 1");
yield return AsyncTestHelper.Await(async () => queryResponse = await LobbyAsyncRequests.Instance.RetrieveLobbyListAsync());
Debug.Log("Getting Lobby List 1");
Debug.Log("Got Lobby List 1");
yield return AsyncTestHelper.Await(
async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#0)");
int numLobbiesIni = queryResponse.Results?.Count ?? 0;

Lobby createResponse = null;
Lobby createLobby = null;
createResponse = await LobbyAsyncRequests.Instance.CreateLobbyAsync(
createLobby = await m_LobbyManager.CreateLobbyAsync(
Assert.IsNotNull(createResponse, "CreateLobbyAsync should return a non-null result.");
m_workingLobbyId = createResponse.Id;
Assert.AreEqual(lobbyName, createResponse.Name, "Created lobby should match the provided name.");
Assert.IsNotNull(createLobby, "CreateLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, createLobby.Name, "Created lobby should match the provided name.");
var createLobbyId = createLobby.Id;
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.
yield return AsyncTestHelper.Await(async () => queryResponse = await LobbyAsyncRequests.Instance.RetrieveLobbyListAsync());
Debug.Log("Got Lobby List 2");
yield return AsyncTestHelper.Await(
async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Results.Count, "Queried lobbies list should contain the test lobby.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Name == lobbyName).Count() == 1, "Checking queried lobby for name.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Id == m_workingLobbyId).Count() == 1, "Checking queried lobby for ID.");
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Results.Count,
"Queried lobbies list should contain the test lobby.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Name == lobbyName).Count() == 1,
"Checking queried lobby for name.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Id == createLobbyId).Count() == 1,
"Checking queried lobby for ID.");
// Query for solely the test lobby via GetLobby.
Debug.Log("Getting Lobby");
Lobby lobby = null;
yield return AsyncTestHelper.Await(async ()=> lobby = await LobbyAsyncRequests.Instance.GetLobbyAsync(createResponse.Id));
Debug.Log("Getting current Lobby");
Debug.Log("Got Lobby");
Lobby currentLobby = m_LobbyManager.CurrentLobby;
Assert.IsNotNull(currentLobby, "GetLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, currentLobby.Name, "Checking the lobby we got for name.");
Assert.AreEqual(createLobbyId, currentLobby.Id, "Checking the lobby we got for ID.");
Assert.IsNotNull(lobby, "GetLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, lobby.Name, "Checking the lobby we got for name.");
Assert.AreEqual(m_workingLobbyId, lobby.Id, "Checking the lobby we got for ID.");
Debug.Log("Deleting current Lobby");
yield return AsyncTestHelper.Await(async ()=> await LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_workingLobbyId));
m_workingLobbyId = null;
yield return AsyncTestHelper.Await(async () => await m_LobbyManager.LeaveLobbyAsync());
// Query to ensure the lobby is gone.
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.
createLobbyId = null;
yield return AsyncTestHelper.Await(async () => queryResponse = await LobbyAsyncRequests.Instance.RetrieveLobbyListAsync());
Debug.Log("Got Lobby List 3");
yield return AsyncTestHelper.Await(
async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
// Some error messages might be asynchronous, so to reduce spillover into other tests, just wait here for a bit before proceeding.
yield return new WaitForSeconds(3);
}
/// <summary>

public IEnumerator CreateFailsWithNull()
{
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
createLobby = await LobbyAsyncRequests.Instance.CreateLobbyAsync(
createLobby = await m_LobbyManager.CreateLobbyAsync(
LogAssert.ignoreFailingMessages = false;
Assert.Null(createLobby, "The returned object will be null, so expect to need to handle it.");
yield return
new WaitForSeconds(
3); //Since CreateLobby cannot be queued, we need to give this a buffer before moving on to other tests.
}
LogAssert.ignoreFailingMessages = false;
[UnityTest]
public IEnumerator CooldownTest()
{
var rateLimiter = new RateLimiter(3);
Stopwatch timer = new Stopwatch();
timer.Start();
//pass Through the first request, which triggers the cooldown.
yield return AsyncTestHelper.Await(async () => await rateLimiter.WaitUntilCooldown());
Assert.Null(createLobby, "The returned object will be null, so expect to need to handle it.");
//Should wait for one second total
yield return AsyncTestHelper.Await(async () => await rateLimiter.WaitUntilCooldown());
timer.Stop();
var elapsedMS = timer.ElapsedMilliseconds;
Debug.Log($"Cooldown took {elapsedMS}/{rateLimiter.coolDownMS} milliseconds.");
var difference = Mathf.Abs(elapsedMS - rateLimiter.coolDownMS);
Assert.IsTrue(difference < 50 && difference >= 0);
}
}

127
Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs


using System;
using System.Collections;
using LobbyRelaySample.relay;
using LobbyRelaySample;
using Test.Tools;
using Unity.Services.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
using UnityEngine.TestTools;

/// <summary>
/// Accesses the Authentication and Relay services in order to ensure we can connect to Relay and retrieve a join code.
/// RelayUtp* wraps the Relay API, so go through that in practice. This simply ensures the connection to the Lobby service is functional.
///
/// If the tests pass, you can assume you are connecting to the Relay service itself properly.
/// </summary>
public class RelayRoundTripTests
{
private LobbyRelaySample.Auth.SubIdentity_Authentication m_auth;
private bool m_didSigninComplete = false;
/// <summary>
/// Accesses the Authentication and Relay services in order to ensure we can connect to Relay and retrieve a join code.
/// RelayUtp* wraps the Relay API, so go through that in practice. This simply ensures the connection to the Lobby service is functional.
///
/// If the tests pass, you can assume you are connecting to the Relay service itself properly.
/// </summary>
public class RelayRoundTripTests
{
[OneTimeSetUp]
public void Setup()
{
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; });
}
[OneTimeSetUp]
public void Setup()
{
Auth.Authenticate("testProfile");
}
[OneTimeTearDown]
public void Teardown()
{
m_auth?.Dispose();
}
/// <summary>
/// Create a Relay allocation, request a join code, and then join. Note that this is purely to ensure the service is functioning;
/// in practice, the RelayUtpSetup does more work to bind to the allocation and has slightly different logic for hosts vs. clients.
/// </summary>
[UnityTest]
public IEnumerator DoBaseRoundTrip()
{
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
yield return new WaitForSeconds(1); // To prevent a possible 429 after a previous test.
/// <summary>
/// Create a Relay allocation, request a join code, and then join. Note that this is purely to ensure the service is functioning;
/// in practice, the RelayUtpSetup does more work to bind to the allocation and has slightly different logic for hosts vs. clients.
/// </summary>
[UnityTest]
public IEnumerator DoBaseRoundTrip()
{
yield return new WaitUntil(Auth.DoneAuthenticating);
// Allocation
Allocation allocation = null;
yield return AsyncTestHelper.Await(async () => allocation = await Relay.Instance.CreateAllocationAsync(4));
Guid allocationId = allocation.AllocationId;
var allocationIP = allocation.RelayServer.IpV4;
var allocationPort = allocation.RelayServer.Port;
Assert.NotNull(allocationId);
Assert.NotNull(allocationIP);
Assert.NotNull(allocationPort);
// Allocation
float timeout = 5;
Allocation allocation = null;
RelayAPIInterface.AllocateAsync(4, (a) => { allocation = a; });
while (allocation == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
// Join code retrieval
string joinCode = null;
yield return AsyncTestHelper.Await(async () =>
joinCode = await Relay.Instance.GetJoinCodeAsync(allocationId));
Assert.Greater(timeout, 0, "Timeout Check (Allocate)");
Guid allocationId = allocation.AllocationId;
var allocationIP = allocation.RelayServer.IpV4;
var allocationPort = allocation.RelayServer.Port;
Assert.NotNull(allocationId);
Assert.NotNull(allocationIP);
Assert.NotNull(allocationPort);
// Join code retrieval
timeout = 5;
string joinCode = null;
RelayAPIInterface.GetJoinCodeAsync(allocationId, (j) => { joinCode = j; });
while (joinCode == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
Assert.False(string.IsNullOrEmpty(joinCode));
Assert.Greater(timeout, 0, "Timeout Check (JoinCode)");
Assert.False(string.IsNullOrEmpty(joinCode));
// Joining with the join code
JoinAllocation joinResponse = null;
yield return AsyncTestHelper.Await(async () =>
joinResponse = await Relay.Instance.JoinAllocationAsync(joinCode));
// Joining with the join code
timeout = 5;
JoinAllocation joinResponse = null;
RelayAPIInterface.JoinAsync(joinCode, (j) => { joinResponse = j; });
while (joinResponse == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
Assert.Greater(timeout, 0, "Timeout Check (Join)");
var codeIp = joinResponse.RelayServer.IpV4;
var codePort = joinResponse.RelayServer.Port;
Assert.AreEqual(codeIp, allocationIP);
Assert.AreEqual(codePort, allocationPort);
}
}
var codeIp = joinResponse.RelayServer.IpV4;
var codePort = joinResponse.RelayServer.Port;
Assert.AreEqual(codeIp, allocationIP);
Assert.AreEqual(codePort, allocationPort);
}
}
}

4
Assets/Scripts/GameLobby/Tests/PlayMode/Tests.Play.asmdef


"LobbyRelaySample",
"Unity.Services.Lobbies",
"Unity.Services.Relay",
"Unity.Networking.Transport"
"Unity.Networking.Transport",
"Unity.Services.Core",
"Unity.Services.Authentication"
],
"includePlatforms": [],
"excludePlatforms": [],

24
Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs


using System;
using System.Collections;
using System.Threading.Tasks;
using LobbyRelaySample;
using Test.Tools;
using Unity.Services.Core;
using Unity.Services.Relay.Models;
using UnityEngine;
using UnityEngine.TestTools;

public class UtpTests
{
private class RelayUtpTest : RelayUtpSetupHost
class RelayUtpTest : RelayUtpSetupHost
{
public Action<NetworkEndPoint, bool> OnGetEndpoint { private get; set; }

}
}
private LobbyRelaySample.Auth.SubIdentity_Authentication m_auth;
GameObject m_dummy;
//Only used when testing DTLS
#pragma warning disable CS0414 // This is the "assigned but its value is never used" warning, which will otherwise appear when DTLS is unavailable.

public void Setup()
{
m_dummy = new GameObject();
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; });
#pragma warning disable 4014
TestAuthSetup();
#pragma warning restore 4014
}
async Task TestAuthSetup()
{
await Auth.Authenticate("test");
m_auth?.Dispose();
GameObject.Destroy(m_dummy);
}

#if ENABLE_MANAGED_UNITYTLS
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
yield return new WaitForSeconds(1); // To prevent a possible 429 after a previous test.
yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
RelayUtpTest relaySetup = m_dummy.AddComponent<RelayUtpTest>();
relaySetup.OnGetEndpoint = OnGetEndpoint;

11
Assets/Scripts/GameLobby/UI/CountdownUI.cs


{
/// <summary>
/// After all players ready up for the game, this will show the countdown that occurs.
/// This countdown is purely visual, to give clients a moment if they need to un-ready before entering the game;
/// This countdown is purely visual, to give clients a moment if they need to un-ready before entering the game;
public class CountdownUI : ObserverBehaviour<Countdown.Data>
public class CountdownUI : UIPanelBase
protected override void UpdateObserver(Countdown.Data data)
public void OnTimeChanged(float time)
base.UpdateObserver(data);
if (observed.TimeLeft <= 0)
if (time <= 0)
m_CountDownText.SetText($"Starting in: {observed.TimeLeft:0}"); // Note that the ":0" formatting rounds, not truncates.
m_CountDownText.SetText($"Starting in: {time:0}"); // Note that the ":0" formatting rounds, not truncates.
}
}
}

12
Assets/Scripts/GameLobby/UI/CreateMenuUI.cs


public class CreateMenuUI : UIPanelBase
{
public JoinCreateLobbyUI m_JoinCreateLobbyUI;
private LocalLobby.LobbyData m_ServerRequestData = new LocalLobby.LobbyData { LobbyName = "New Lobby", MaxPlayerCount = 4 };
string m_ServerName;
bool m_IsServerPrivate;
public override void Start()
{

public void SetServerName(string serverName)
{
m_ServerRequestData.LobbyName = serverName;
m_ServerName = serverName;
m_ServerRequestData.Private = priv;
m_IsServerPrivate = priv;
Locator.Get.Messenger.OnReceiveMessage(MessageType.CreateLobbyRequest, m_ServerRequestData);
//Disabled as it's a one-off butto call
#pragma warning disable 4014
GameManager.Instance.CreateLobby(m_ServerName, m_IsServerPrivate);
#pragma warning restore 4014
}
}
}

30
Assets/Scripts/GameLobby/UI/DisplayCodeUI.cs


using System;
using TMPro;
using UnityEngine;

/// Watches a lobby or relay code for updates, displaying the current code to lobby members.
/// </summary>
public class DisplayCodeUI : ObserverPanel<LocalLobby>
public class DisplayCodeUI : UIPanelBase
{
public enum CodeType { Lobby = 0, Relay = 1 }

CodeType m_codeType;
public override void ObservedUpdated(LocalLobby observed)
void LobbyCodeChanged(string newCode)
string code = m_codeType == CodeType.Lobby ? observed.LobbyCode : observed.RelayCode;
if (!string.IsNullOrEmpty(code))
if (!string.IsNullOrEmpty(newCode))
m_outputText.text = code;
m_outputText.text = newCode;
Show();
}
else

}
public override void Start()
{
base.Start();
if(m_codeType==CodeType.Lobby)
Manager.LocalLobby.LobbyCode.onChanged += LobbyCodeChanged;
if(m_codeType==CodeType.Relay)
Manager.LocalLobby.RelayCode.onChanged += LobbyCodeChanged;
}
void OnDestroy()
{
if (Manager == null)
return;
if(m_codeType==CodeType.Lobby)
Manager.LocalLobby.LobbyCode.onChanged -= LobbyCodeChanged;
if(m_codeType==CodeType.Relay)
Manager.LocalLobby.RelayCode.onChanged -= LobbyCodeChanged;
}
}
}

2
Assets/Scripts/GameLobby/UI/EmoteButtonUI.cs


public class EmoteButtonUI : MonoBehaviour
{
[SerializeField]
private EmoteType m_emoteType;
EmoteType m_emoteType;
public void SetPlayerEmote()
{

24
Assets/Scripts/GameLobby/UI/GameStateVisibilityUI.cs


/// <summary>
/// Show or hide a UI element based on the current GameState (e.g. in a lobby).
/// </summary>
[RequireComponent(typeof(LocalMenuStateObserver))]
public class GameStateVisibilityUI : ObserverPanel<LocalMenuState>
public class GameStateVisibilityUI : UIPanelBase
public override void ObservedUpdated(LocalMenuState observed)
void GameStateChanged(GameState state)
if (!ShowThisWhen.HasFlag(observed.State))
if (!ShowThisWhen.HasFlag(state))
{
}
Debug.Log($"GameStateChanged for {gameObject.name} : {state}");
}
public override void Start()
{
base.Start();
Manager.onGameStateChanged += GameStateChanged;
}
void OnDestroy()
{
if (Manager == null)
return;
Manager.onGameStateChanged -= GameStateChanged;
}
}
}

30
Assets/Scripts/GameLobby/UI/InLobbyUserList.cs


namespace LobbyRelaySample.UI
{
/// <summary>
/// Contains the InLobbyUserUI instances while showing the UI for a lobby.
/// </summary>
[RequireComponent(typeof(LocalLobbyObserver))]
public class InLobbyUserList : ObserverPanel<LocalLobby>
public class InLobbyUserList : UIPanelBase
List<string> m_CurrentUsers = new List<string>(); // Just for keeping track more easily of which users are already displayed.
List<string>
m_CurrentUsers =
new List<string>(); // Just for keeping track more easily of which users are already displayed.
/// <summary>
/// When the observed data updates, we need to detect changes to the list of players.
/// </summary>
public override void ObservedUpdated(LocalLobby observed)
public override void Start()
for (int id = m_CurrentUsers.Count - 1; id >= 0; id--) // We might remove users if they aren't in the new data, so iterate backwards.
base.Start();
GameManager.Instance.LocalLobby.onUserListChanged += OnUsersChanged;
}
void OnUsersChanged(Dictionary<string, LocalPlayer> newUserDict)
{
for (int id = m_CurrentUsers.Count - 1;
id >= 0;
id--) // We might remove users if they aren't in the new data, so iterate backwards.
if (!observed.LobbyUsers.ContainsKey(userId))
if (!newUserDict.ContainsKey(userId))
{
foreach (var ui in m_UserUIObjects)
{

}
}
foreach (var lobbyUserKvp in observed.LobbyUsers) // If there are new players, we need to hook them into the UI.
foreach (var lobbyUserKvp in newUserDict) // If there are new players, we need to hook them into the UI.
{
if (m_CurrentUsers.Contains(lobbyUserKvp.Key))
continue;

65
Assets/Scripts/GameLobby/UI/InLobbyUserUI.cs


/// <summary>
/// When inside a lobby, this will show information about a player, whether local or remote.
/// </summary>
[RequireComponent(typeof(LobbyUserObserver))]
public class InLobbyUserUI : ObserverPanel<LobbyUser>
public class InLobbyUserUI : UIPanelBase
{
[SerializeField]
TMP_Text m_DisplayNameText;

[SerializeField]
Image m_EmoteImage;

vivox.VivoxUserHandler m_vivoxUserHandler;
public bool IsAssigned => UserId != null;
public string UserId { get; set; }
LocalPlayer m_localPlayer;
public string UserId { get; private set; }
private LobbyUserObserver m_observer;
public void SetUser(LobbyUser myLobbyUser)
public void SetUser(LocalPlayer myLocalPlayer)
if (m_observer == null)
m_observer = GetComponent<LobbyUserObserver>();
m_observer.BeginObserving(myLobbyUser);
UserId = myLobbyUser.ID;
m_localPlayer = myLocalPlayer;
SubscribeToPlayerUpdates();
UserId = myLocalPlayer.ID.Value;
public void SubscribeToPlayerUpdates()
{
m_localPlayer.DisplayName.onChanged += SetDisplayName;
m_localPlayer.UserStatus.onChanged += SetUserStatus;
m_localPlayer.Emote.onChanged += SetEmote;
m_localPlayer.IsHost.onChanged += SetIsHost;
}
public void UnsubscribeToPlayerUpdates()
{
m_localPlayer.DisplayName.onChanged -= SetDisplayName;
m_localPlayer.UserStatus.onChanged -= SetUserStatus;
m_localPlayer.Emote.onChanged -= SetEmote;
m_localPlayer.IsHost.onChanged -= SetIsHost;
}
m_observer.EndObserving();
UnsubscribeToPlayerUpdates();
m_localPlayer = null;
public override void ObservedUpdated(LobbyUser observed)
void SetDisplayName(string displayName)
m_DisplayNameText.SetText(observed.DisplayName);
m_StatusText.SetText(SetStatusFancy(observed.UserStatus));
m_EmoteImage.sprite = EmoteIcon(observed.Emote);
m_HostIcon.enabled = observed.IsHost;
m_DisplayNameText.SetText(displayName);
void SetUserStatus(UserStatus statusText)
{
m_StatusText.SetText(SetStatusFancy(statusText));
}
void SetEmote(EmoteType emote)
{
m_EmoteImage.sprite = EmoteIcon(emote);
}
void SetIsHost(bool isHost)
{
m_HostIcon.enabled = isHost;
}
/// <summary>
/// EmoteType to Icon Sprite
/// m_EmoteIcon[0] = Smile

22
Assets/Scripts/GameLobby/UI/JoinCreateLobbyUI.cs


/// <summary>
/// The panel that holds the lobby joining and creation panels.
/// </summary>
public class JoinCreateLobbyUI : ObserverPanel<LocalMenuState>
public class JoinCreateLobbyUI : UIPanelBase
[HideInInspector]
public UnityEvent<JoinCreateTabs> m_OnTabChanged;
[SerializeField] //Serialized for Visisbility in Editor

CurrentTab = JoinCreateTabs.Create;
}
public override void ObservedUpdated(LocalMenuState observed)
void GameStateChanged(GameState state)
if (observed.State == GameState.JoinMenu)
if (state == GameState.JoinMenu)
{
m_OnTabChanged?.Invoke(m_CurrentTab);
Show(false);

Hide();
}
}
public override void Start()
{
base.Start();
Manager.onGameStateChanged += GameStateChanged;
}
void OnDestroy()
{
if (Manager == null)
return;
Manager.onGameStateChanged -= GameStateChanged;
}
}
}

40
Assets/Scripts/GameLobby/UI/JoinMenuUI.cs


/// <summary>
/// Handles the list of LobbyButtons and ensures it stays synchronized with the lobby list from the service.
/// </summary>
public class JoinMenuUI : ObserverPanel<LobbyServiceData>
public class JoinMenuUI : UIPanelBase
{
[SerializeField]
LobbyButtonUI m_LobbyButtonPrefab;

Dictionary<string, LocalLobby> m_LocalLobby = new Dictionary<string, LocalLobby>();
/// <summary>Contains some amount of information used to join an existing lobby.</summary>
LocalLobby.LobbyData m_LocalLobbySelected;
LocalLobby m_LocalLobbySelected;
Manager.LobbyList.onLobbyListChange += OnLobbyListChanged;
}
void OnDestroy()
{
if (Manager == null)
return;
Manager.LobbyList.onLobbyListChange -= OnLobbyListChanged;
}
void OnTabChanged(JoinCreateTabs tabState)

public void LobbyButtonSelected(LocalLobby lobby)
{
m_LocalLobbySelected = lobby.Data;
m_LocalLobbySelected = lobby;
m_LocalLobbySelected = new LocalLobby.LobbyData(newCode.ToUpper());
m_LocalLobbySelected.LobbyCode.Value = newCode.ToUpper();
m_LocalLobbySelected = default;
m_LocalLobbySelected = new LocalLobby();
}
public void OnRefresh()

public override void ObservedUpdated(LobbyServiceData observed)
void OnLobbyListChanged(Dictionary<string, LocalLobby> lobbyList)
foreach (var codeLobby in observed.CurrentLobbies)
foreach (var codeLobby in lobbyList)
{
var lobbyCodeKey = codeLobby.Key;
var lobbyData = codeLobby.Value;

else
{
if (CanDisplay(lobbyData))
UpdateLobbyButton(lobbyCodeKey, lobbyData);
SetLobbyButton(lobbyCodeKey, lobbyData);
else
RemoveLobbyButton(lobbyData);
}

Locator.Get.Messenger.OnReceiveMessage(MessageType.QuickJoin, null);
}
private bool CanDisplay(LocalLobby lobby)
bool CanDisplay(LocalLobby lobby)
return lobby.Data.State == LobbyState.Lobby && !lobby.Private;
return lobby.LocalLobbyState.Value == LobbyState.Lobby && !lobby.Private.Value;
private void AddNewLobbyButton(string lobbyCode, LocalLobby lobby)
void AddNewLobbyButton(string lobbyCode, LocalLobby lobby)
{
var lobbyButtonInstance = Instantiate(m_LobbyButtonPrefab, m_LobbyButtonParent);
lobbyButtonInstance.GetComponent<LocalLobbyObserver>().BeginObserving(lobby);

m_LocalLobby.Add(lobbyCode, lobby);
}
private void UpdateLobbyButton(string lobbyCode, LocalLobby lobby)
void SetLobbyButton(string lobbyCode, LocalLobby lobby)
m_LobbyButtons[lobbyCode].UpdateLobby(lobby);
m_LobbyButtons[lobbyCode].SetLobby(lobby);
private void RemoveLobbyButton(LocalLobby lobby)
void RemoveLobbyButton(LocalLobby lobby)
var lobbyID = lobby.LobbyID;
var lobbyID = lobby.LobbyID.Value;
var lobbyButton = m_LobbyButtons[lobbyID];
lobbyButton.GetComponent<LocalLobbyObserver>().EndObserving();
m_LobbyButtons.Remove(lobbyID);

}
}
}

26
Assets/Scripts/GameLobby/UI/LobbyButtonUI.cs


using System;
using TMPro;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Controls an entry in the join menu's list of lobbies, acting as a clickable button as well as displaying info about the lobby.
/// </summary>
[RequireComponent(typeof(LocalLobbyObserver))]
//TODO WHAT WAS THIS OBSERVING??!?
public class LobbyButtonUI : MonoBehaviour
{
[SerializeField]

/// </summary>
public UnityEvent<LocalLobby> onLobbyPressed;
LocalLobbyObserver m_DataObserver;
void Awake()
{
m_DataObserver = GetComponent<LocalLobbyObserver>();
}
LocalLobby m_Lobby;
/// <summary>
/// UI CallBack

onLobbyPressed?.Invoke(m_DataObserver.observed);
}
public void UpdateLobby(LocalLobby lobby)
{
m_DataObserver.observed.CopyObserved(lobby);
//TODO Select Lobby
public void OnLobbyUpdated(LocalLobby data)
public void SetLobby(LocalLobby lobby)
lobbyNameText.SetText(data.LobbyName);
lobbyCountText.SetText($"{data.PlayerCount}/{data.MaxPlayerCount}");
m_Lobby = lobby;
lobbyNameText.SetText(m_Lobby.LobbyName.Value);
lobbyCountText.SetText($"{m_Lobby.PlayerCount}/{m_Lobby.MaxPlayerCount}");
}
}

7
Assets/Scripts/GameLobby/UI/LobbyNameUI.cs


/// <summary>
/// Displays the name of the lobby.
/// </summary>
public class LobbyNameUI : ObserverPanel<LocalLobby>
public class LobbyNameUI : UIPanelBase
public override void ObservedUpdated(LocalLobby observed)
public override void Start()
m_lobbyNameText.SetText(observed.LobbyName);
base.Start();
Manager.LocalLobby.LobbyName.onChanged += (s) => { m_lobbyNameText.SetText(s); };
}
}
}

12
Assets/Scripts/GameLobby/UI/LobbyUserVolumeUI.cs


public class LobbyUserVolumeUI : MonoBehaviour
{
[SerializeField]
private UIPanelBase m_volumeSliderContainer;
UIPanelBase m_volumeSliderContainer;
private UIPanelBase m_muteToggleContainer;
UIPanelBase m_muteToggleContainer;
private GameObject m_muteIcon;
GameObject m_muteIcon;
private GameObject m_micMuteIcon;
GameObject m_micMuteIcon;
private Slider m_volumeSlider;
Slider m_volumeSlider;
private Toggle m_muteToggle;
Toggle m_muteToggle;
/// <param name="shouldResetUi">
/// When the user is being added, we want the UI to reset to the default values.

56
Assets/Scripts/GameLobby/UI/RateLimitVisibility.cs


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;
/// <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] LobbyManager.RequestType m_requestType;
private void Start()
{
LobbyAsyncRequests.Instance.GetRateLimit(m_requestType).onCooldownChange += UpdateVisibility;
}
private void OnDestroy()
{
LobbyAsyncRequests.Instance.GetRateLimit(m_requestType).onCooldownChange -= UpdateVisibility;
}
void Start()
{
GameManager.Instance.LobbyManager.GetRateLimit(m_requestType).onCooldownChange += UpdateVisibility;
}
private void UpdateVisibility(bool isCoolingDown)
{
if (isCoolingDown)
m_target.Hide(m_alphaWhenHidden);
else
m_target.Show();
}
}
void OnDestroy()
{
if (GameManager.Instance == null || GameManager.Instance.LobbyManager == null)
return;
GameManager.Instance.LobbyManager.GetRateLimit(m_requestType).onCooldownChange -= UpdateVisibility;
}
void UpdateVisibility(bool isCoolingDown)
{
if (isCoolingDown)
m_target.Hide(m_alphaWhenHidden);
else
m_target.Show();
}
}
}

2
Assets/Scripts/GameLobby/UI/ReadyCheckUI.cs


{
ChangeState(UserStatus.Lobby);
}
private void ChangeState(UserStatus status)
void ChangeState(UserStatus status)
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.LobbyUserStatus, status);
}

16
Assets/Scripts/GameLobby/UI/RecolorForLobbyType.cs


/// We want to illustrate filtering the lobby list by some arbitrary variable. This will allow the lobby host to choose a color for the lobby, and will display a lobby's current color.
/// (Note that this isn't sent over Relay to other clients for realtime updates.)
/// </summary>
[RequireComponent(typeof(LocalLobbyObserver))]
public class RecolorForLobbyType : MonoBehaviour
{
private static readonly Color s_orangeColor = new Color(0.8352942f, 0.3686275f, 0);

private Graphic[] m_toRecolor;
private LocalLobby m_lobby;
public void UpdateLobby(LocalLobby lobby)
public void Start()
m_lobby = lobby;
Color color = s_colorsOrdered[(int)lobby.Color];
m_lobby = GameManager.Instance.LocalLobby;
m_lobby.LocalLobbyColor.onChanged += ChangeColors;
}
public void ChangeColors(LobbyColor lobbyColor)
{
Color color = s_colorsOrdered[(int)lobbyColor];
foreach (Graphic graphic in m_toRecolor)
graphic.color = new Color(color.r, color.g, color.b, graphic.color.a);
}

/// Triggers the ChangeColors method above
m_lobby.Color = (LobbyColor)color;
m_lobby.LocalLobbyColor.Value = (LobbyColor)color;
}
}
}

14
Assets/Scripts/GameLobby/UI/RelayAddressUI.cs


/// <summary>
/// Displays the IP when connected to Relay.
/// </summary>
public class RelayAddressUI : ObserverPanel<LocalLobby>
public class RelayAddressUI : UIPanelBase
public override void ObservedUpdated(LocalLobby observed)
public override void Start()
m_IPAddressText.SetText(observed.RelayServer?.ToString());
base.Start();
GameManager.Instance.LocalLobby.RelayServer.onChanged += GotRelayAddress;
}
void GotRelayAddress(ServerAddress address)
{
m_IPAddressText.SetText(address.ToString());
}
}

53
Assets/Scripts/GameLobby/UI/ShowWhenLobbyStateUI.cs


using UnityEngine;
namespace LobbyRelaySample.UI
{
using System;
using UnityEngine;
namespace LobbyRelaySample.UI
{
/// </summary>
public class ShowWhenLobbyStateUI : ObserverPanel<LocalLobby>
{
[SerializeField]
private LobbyState m_ShowThisWhen;
public override void ObservedUpdated(LocalLobby observed)
{
if (m_ShowThisWhen.HasFlag(observed.State))
Show();
else
Hide();
}
}
}
/// </summary>
public class ShowWhenLobbyStateUI : UIPanelBase
{
[SerializeField]
LobbyState m_ShowThisWhen;
public void LobbyChanged(LobbyState lobbyState)
{
if (m_ShowThisWhen.HasFlag(lobbyState))
Show();
else
Hide();
}
public override void Start()
{
base.Start();
Manager.LocalLobby.LocalLobbyState.onChanged += LobbyChanged;
}
public void OnDestroy()
{
if (GameManager.Instance == null)
return;
Manager.LocalLobby.LocalLobbyState.onChanged -= LobbyChanged;
}
}
}

38
Assets/Scripts/GameLobby/UI/SpinnerUI.cs


using System;
using System.Text;
using TMPro;
using UnityEngine;

/// <summary>
/// Controls a simple throbber that is displayed when the lobby list is being refreshed.
/// </summary>
public class SpinnerUI : ObserverPanel<LobbyServiceData>
public class SpinnerUI : UIPanelBase
[SerializeField] private TMP_Text m_errorText;
[SerializeField] private UIPanelBase m_spinnerImage;
[SerializeField] private UIPanelBase m_noServerText;
[SerializeField] private UIPanelBase m_errorTextVisibility;
[SerializeField] TMP_Text m_errorText;
[SerializeField] UIPanelBase m_spinnerImage;
[SerializeField] UIPanelBase m_noServerText;
[SerializeField] UIPanelBase m_errorTextVisibility;
[SerializeField] private UIPanelBase m_raycastBlocker;
[SerializeField] UIPanelBase m_raycastBlocker;
public override void ObservedUpdated(LobbyServiceData observed)
public override void Start()
if (observed.State == LobbyQueryState.Fetching)
base.Start();
Manager.LobbyList.QueryState.onChanged += QueryStateChanged;
}
void OnDestroy()
{
if (Manager == null)
return;
Manager.LobbyList.QueryState.onChanged -= QueryStateChanged;
}
void QueryStateChanged(LobbyQueryState state)
{
if (state == LobbyQueryState.Fetching)
{
Show();
m_spinnerImage.Show();

}
else if (observed.State == LobbyQueryState.Error)
else if (state == LobbyQueryState.Error)
{
m_spinnerImage.Hide();
m_raycastBlocker.Hide();

else if (observed.State == LobbyQueryState.Fetched)
else if (state == LobbyQueryState.Fetched)
if (observed.CurrentLobbies.Count < 1)
if (Manager.LobbyList.CurrentLobbies.Count < 1)
{
m_noServerText.Show();
}

12
Assets/Scripts/GameLobby/UI/UIPanelBase.cs


private UnityEvent<bool> m_onVisibilityChange;
bool showing;
protected GameManager Manager
{
get
{
if (m_gameManager != null) return m_gameManager;
return m_gameManager = GameManager.Instance;
}
}
GameManager m_gameManager;
CanvasGroup m_canvasGroup;
List<UIPanelBase> m_uiPanelsInChildren = new List<UIPanelBase>(); // Otherwise, when this Shows/Hides, the children won't know to update their own visibility.

{
Show(true);
}
public void Show(bool propagateToChildren)
{
MyCanvasGroup.alpha = 1;

15
Assets/Scripts/GameLobby/UI/UserNameUI.cs


using LobbyRelaySample.ngo;
using TMPro;
using UnityEngine;

/// Displays the player's name.
/// </summary>
public class UserNameUI : ObserverPanel<LobbyUser>
public class UserNameUI : UIPanelBase
public override void ObservedUpdated(LobbyUser observed)
public override async void Start()
{
base.Start();
var localUser = await GameManager.Instance.LocalUserInitialized();
localUser.DisplayName.onChanged += SetText;
}
void SetText(string text)
m_TextField.SetText(observed.DisplayName);
m_TextField.SetText(text);
}
}
}

42
Assets/Scripts/GameLobby/UI/UserStateVisibilityUI.cs


}
/// <summary>
/// Shows the UI when the LobbyUser matches some conditions, including having the target permissions.
/// Shows the UI when the LocalPlayer matches some conditions, including having the target permissions.
[RequireComponent(typeof(LobbyUserObserver))]
public class UserStateVisibilityUI : ObserverPanel<LobbyUser>
public class UserStateVisibilityUI : UIPanelBase
bool m_HasStatusFlags = false;
bool m_HasPermissions;
public override void ObservedUpdated(LobbyUser observed)
public override async void Start()
var hasStatusFlags = ShowThisWhen.HasFlag(observed.UserStatus);
base.Start();
var localUser = await Manager.LocalUserInitialized();
localUser.IsHost.onChanged += OnUserHostChanged;
var hasPermissions = false;
localUser.UserStatus.onChanged += OnUserStatusChanged;
}
if (Permissions.HasFlag(UserPermission.Host) && observed.IsHost)
void OnUserStatusChanged(UserStatus observedStatus)
{
m_HasStatusFlags = ShowThisWhen.HasFlag(observedStatus);
CheckVisibility();
}
void OnUserHostChanged(bool isHost)
{
m_HasPermissions = false;
if (Permissions.HasFlag(UserPermission.Host) && isHost)
hasPermissions = true;
m_HasPermissions = true;
else if (Permissions.HasFlag(UserPermission.Client) && !observed.IsHost)
else if (Permissions.HasFlag(UserPermission.Client) && !isHost)
hasPermissions = true;
m_HasPermissions = true;
if (hasStatusFlags && hasPermissions)
CheckVisibility();
}
void CheckVisibility()
{
if (m_HasStatusFlags && m_HasPermissions)
}
}

3
Assets/Scripts/GameLobby/Vivox/VivoxSetup.cs


using System;
using System.Collections.Generic;
using Unity.Services.Authentication;
using Unity.Services.Vivox;
using VivoxUnity;

m_userHandlers = userHandlers;
VivoxService.Instance.Initialize();
Account account = new Account(Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id"));
Account account = new Account(AuthenticationService.Instance.PlayerId);
m_loginSession = VivoxService.Instance.Client.GetLoginSession(account);
string token = m_loginSession.GetLoginToken();

37
Packages/manifest.json


{
"dependencies": {
"com.unity.2d.sprite": "1.0.0",
"com.unity.collab-proxy": "1.15.15",
"com.unity.collections": "1.0.0-pre.6",
"com.unity.ide.rider": "3.0.13",
"com.unity.ide.visualstudio": "2.0.14",
"com.unity.collab-proxy": "1.17.2",
"com.unity.collections": "1.2.4",
"com.unity.ide.rider": "3.0.15",
"com.unity.ide.visualstudio": "2.0.16",
"com.unity.netcode.gameobjects": "1.0.0-pre.9",
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.netcode.gameobjects": "1.0.2",
"com.unity.nuget.newtonsoft-json": "3.0.2",
"com.unity.services.authentication": "2.0.0",
"com.unity.services.core": "1.2.0",
"com.unity.services.lobby": "1.0.0-pre.7",
"com.unity.services.relay": "1.0.1-pre.3",
"com.unity.services.authentication": "2.2.0",
"com.unity.services.core": "1.4.2",
"com.unity.services.lobby": "1.0.3",
"com.unity.services.relay": "1.0.4",
"com.unity.services.wire": "1.0.0",
"com.unity.services.wire": "1.1.1",
"com.unity.transport": "1.0.0-pre.9",
"com.unity.transport": "1.1.0",
"com.unity.ugui": "1.0.0",
"com.unity.modules.ai": "1.0.0",
"com.unity.modules.androidjni": "1.0.0",

"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0"
},
"scopedRegistries": [
{
"name": "Internal Candidates Registry",
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates",
"scopes": [
"com.unity.services.wire",
"com.unity.services.lobby",
"com.unity.services.authentication"
]
}
]
}
}

91
Packages/packages-lock.json


"dependencies": {}
},
"com.unity.burst": {
"version": "1.6.5",
"version": "1.6.6",
"depth": 1,
"source": "registry",
"dependencies": {

},
"com.unity.collab-proxy": {
"version": "1.15.15",
"version": "1.17.2",
"depth": 0,
"source": "registry",
"dependencies": {

},
"com.unity.collections": {
"version": "1.2.3",
"depth": 2,
"version": "1.2.4",
"depth": 0,
"com.unity.burst": "1.6.4",
"com.unity.burst": "1.6.6",
"com.unity.test-framework": "1.1.31"
},
"url": "https://packages.unity.com"

"url": "https://packages.unity.com"
},
"com.unity.ide.rider": {
"version": "3.0.13",
"version": "3.0.15",
"depth": 0,
"source": "registry",
"dependencies": {

},
"com.unity.ide.visualstudio": {
"version": "2.0.14",
"version": "2.0.16",
"depth": 0,
"source": "registry",
"dependencies": {

"url": "https://packages.unity.com"
},
"com.unity.mathematics": {
"version": "1.2.5",
"version": "1.2.6",
"depth": 1,
"source": "registry",
"dependencies": {},

"version": "1.0.0-pre.9",
"version": "1.0.2",
"com.unity.transport": "1.0.0"
"com.unity.transport": "1.2.0"
},
"url": "https://packages.unity.com"
},

},
"com.unity.nuget.newtonsoft-json": {
"version": "3.0.2",
"depth": 1,
"depth": 0,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"

"url": "https://packages.unity.com"
},
"com.unity.services.authentication": {
"version": "2.0.0",
"version": "2.2.0",
"com.unity.services.core": "1.3.1",
"com.unity.modules.unitywebrequest": "1.0.0"
"com.unity.services.core": "1.4.3",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.ugui": "1.0.0"
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates"
"url": "https://packages.unity.com"
"version": "1.3.1",
"version": "1.4.3",
"depth": 1,
"source": "registry",
"dependencies": {

"url": "https://packages.unity.com"
},
"com.unity.services.lobby": {
"version": "1.0.0-pre.7",
"version": "1.0.3",
"com.unity.services.core": "1.1.0-pre.77",
"com.unity.services.core": "1.4.0",
"com.unity.nuget.newtonsoft-json": "3.0.1",
"com.unity.services.authentication": "1.0.0-pre.73"
"com.unity.nuget.newtonsoft-json": "3.0.2",
"com.unity.services.authentication": "2.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.services.qos": {
"version": "1.0.2",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.services.core": "1.4.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.nuget.newtonsoft-json": "3.0.2",
"com.unity.services.authentication": "2.0.0",
"com.unity.collections": "1.2.4"
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates"
"url": "https://packages.unity.com"
"version": "1.0.1-pre.3",
"version": "1.0.4",
"com.unity.services.core": "1.1.0-pre.10",
"com.unity.services.core": "1.4.0",
"com.unity.services.authentication": "2.0.0",
"com.unity.services.qos": "1.0.2",
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.authentication": "1.0.0-pre.6",
"com.unity.transport": "1.0.0-pre.6"
"com.unity.nuget.newtonsoft-json": "3.0.2",
"com.unity.transport": "1.1.0"
},
"url": "https://packages.unity.com"
},

"url": "https://packages.unity.com"
},
"com.unity.services.wire": {
"version": "1.0.0",
"version": "1.1.1",
"com.unity.services.core": "1.2.0",
"com.unity.nuget.newtonsoft-json": "3.0.1",
"com.unity.services.authentication": "1.0.0-pre.37"
"com.unity.services.core": "1.4.2",
"com.unity.nuget.newtonsoft-json": "3.0.2",
"com.unity.services.authentication": "2.1.1"
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates"
"url": "https://packages.unity.com"
},
"com.unity.settings-manager": {
"version": "2.0.0",

"url": "https://packages.unity.com"
},
"com.unity.transport": {
"version": "1.0.0",
"version": "1.2.0",
"com.unity.collections": "1.2.3",
"com.unity.burst": "1.6.4",
"com.unity.mathematics": "1.2.5"
"com.unity.collections": "1.2.4",
"com.unity.burst": "1.6.6",
"com.unity.mathematics": "1.2.6"
},
"url": "https://packages.unity.com"
},

"com.unity.modules.ui": "1.0.0",
"com.unity.modules.imgui": "1.0.0"
}
},
"com.veriorpies.parrelsync": {
"version": "file:ParrelSync",
"depth": 0,
"source": "embedded",
"dependencies": {}
},
"com.unity.modules.ai": {
"version": "1.0.0",

15
ProjectSettings/PackageManagerSettings.asset


m_Scopes: []
m_IsDefault: 1
m_Capabilities: 7
- m_Id: scoped:Internal Candidates Registry
m_Name: Internal Candidates Registry
m_Url: https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates
m_Scopes:
- com.unity.services.wire
- com.unity.services.lobby
- com.unity.services.authentication
m_IsDefault: 0
m_Capabilities: 0
m_UserSelectedRegistryName: Internal Candidates Registry
m_UserSelectedRegistryName:
m_UserModificationsInstanceId: -824
m_OriginalInstanceId: -828
m_UserModificationsInstanceId: -822
m_OriginalInstanceId: -826
m_LoadAssets: 0

2
ProjectSettings/RiderScriptEditorPersistedState.asset


m_Script: {fileID: 0}
m_Name:
m_EditorClassIdentifier: Unity.Rider.Editor:Packages.Rider.Editor:RiderScriptEditorPersistedState
lastWriteTicks: -8585486268141277427
lastWriteTicks: -8585383256260152102

2
Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs.meta


fileFormatVersion: 2
guid: edfa4d53eaae84c43ba581088940700c
guid: 3167ee4f02a6e4b4bb4d4e8830ea69c5
MonoImporter:
externalObjects: {}
serializedVersion: 2

118
Assets/Scripts/GameLobby/Auth/Auth.cs


using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;
using UnityEngine;
namespace LobbyRelaySample
{
public enum AuthState
{
Initialized,
Authenticating,
Authenticated,
Error,
TimedOut
}
public static class Auth
{
public static AuthState AuthenticationState { get; private set; } = AuthState.Initialized;
public static async Task<AuthState> Authenticate(string profile,int tries = 5)
{
//If we are already authenticated, just return Auth
if (AuthenticationState == AuthState.Authenticated)
{
return AuthenticationState;
}
if (AuthenticationState == AuthState.Authenticating)
{
Debug.LogWarning("Cant Authenticate if we are authenticating or authenticated");
await Authenticating();
return AuthenticationState;
}
var profileOptions = new InitializationOptions();
profileOptions.SetProfile(profile);
await UnityServices.InitializeAsync(profileOptions);
await SignInAnonymouslyAsync(tries);
Debug.Log($"Auth attempts Finished : {AuthenticationState.ToString()}");
return AuthenticationState;
}
//Awaitable task that will pass the clientID once authentication is done.
public static string ID()
{
return AuthenticationService.Instance.PlayerId;
}
//Awaitable task that will pass once authentication is done.
public static async Task<AuthState> Authenticating()
{
while (AuthenticationState == AuthState.Authenticating || AuthenticationState == AuthState.Initialized)
{
await Task.Delay(200);
}
return AuthenticationState;
}
public static bool DoneAuthenticating()
{
return AuthenticationState != AuthState.Authenticating &&
AuthenticationState != AuthState.Initialized;
}
static async Task SignInAnonymouslyAsync(int maxRetries)
{
AuthenticationState = AuthState.Authenticating;
var tries = 0;
while (AuthenticationState == AuthState.Authenticating && tries < maxRetries)
{
try
{
//To ensure staging login vs non staging
await AuthenticationService.Instance.SignInAnonymouslyAsync();
if (AuthenticationService.Instance.IsSignedIn && AuthenticationService.Instance.IsAuthorized)
{
AuthenticationState = AuthState.Authenticated;
break;
}
}
catch (AuthenticationException ex)
{
// Compare error code to AuthenticationErrorCodes
// Notify the player with the proper error message
Debug.LogError(ex);
AuthenticationState = AuthState.Error;
}
catch (RequestFailedException exception)
{
// Compare error code to CommonErrorCodes
// Notify the player with the proper error message
Debug.LogError(exception);
AuthenticationState = AuthState.Error;
}
tries++;
await Task.Delay(1000);
}
if (AuthenticationState != AuthState.Authenticated)
{
Debug.LogWarning($"Player was not signed in successfully after {tries} attempts");
AuthenticationState = AuthState.TimedOut;
}
}
public static void SignOut()
{
AuthenticationService.Instance.SignOut(false);
AuthenticationState = AuthState.Initialized;
}
}
}

44
Assets/Scripts/GameLobby/Game/LocalLobbyList.cs


using System;
using System.Collections.Generic;
namespace LobbyRelaySample
{
/// <summary>
/// Used when displaying the lobby list, to indicate when we are awaiting an updated lobby query.
/// </summary>
public enum LobbyQueryState
{
Empty,
Fetching,
Error,
Fetched
}
/// <summary>
/// Holds data related to the Lobby service itself - The latest retrieved lobby list, the state of retrieval.
/// </summary>
[System.Serializable]
public class LocalLobbyList
{
LobbyQueryState m_CurrentState = LobbyQueryState.Empty;
public CallbackValue<LobbyQueryState> QueryState = new CallbackValue<LobbyQueryState>();
public Action<Dictionary<string, LocalLobby>> onLobbyListChange;
Dictionary<string, LocalLobby> m_currentLobbies = new Dictionary<string, LocalLobby>();
/// <summary>
/// Maps from a lobby's ID to the local representation of it. This allows us to remember which remote lobbies are which LocalLobbies.
/// Will only trigger if the dictionary is set wholesale. Changes in the size or contents will not trigger OnChanged.
/// </summary>
public Dictionary<string, LocalLobby> CurrentLobbies
{
get { return m_currentLobbies; }
set
{
m_currentLobbies = value;
onLobbyListChange?.Invoke(m_currentLobbies);
}
}
}
}

55
Assets/Scripts/GameLobby/Game/LocalPlayer.cs


using System;
namespace LobbyRelaySample
{
/// <summary>
/// Current state of the user in the lobby.
/// This is a Flags enum to allow for the Inspector to select multiples for various UI features.
/// </summary>
[Flags]
public enum UserStatus
{
None = 0,
Connecting = 1, // User has joined a lobby but has not yet connected to Relay.
Lobby = 2, // User is in a lobby and connected to Relay.
Ready = 4, // User has selected the ready button, to ready for the "game" to start.
InGame = 8, // User is part of a "game" that has started.
Menu = 16 // User is not in a lobby, in one of the main menus.
}
/// <summary>
/// Data for a local player instance. This will update data and is observed to know when to push local player changes to the entire lobby.
/// </summary>
[Serializable]
public class LocalPlayer : Observed<LocalPlayer>
{
public CallbackValue<bool> IsHost = new CallbackValue<bool>(false);
public CallbackValue<string> DisplayName = new CallbackValue<string>("");
public CallbackValue<EmoteType> Emote = new CallbackValue<EmoteType>(EmoteType.None);
public CallbackValue<UserStatus> UserStatus = new CallbackValue<UserStatus>((UserStatus)0);
public CallbackValue<string> ID = new CallbackValue<string>("");
public LocalPlayer(string id, bool isHost, string displayName,
EmoteType emote = default, UserStatus status = default)
{
IsHost.Value = isHost;
DisplayName.Value = displayName;
Emote.Value = emote;
UserStatus.Value = status;
ID.Value = id;
}
public void ResetState()
{
IsHost.Value = false;
Emote.Value = EmoteType.None;
UserStatus.Value = LobbyRelaySample.UserStatus.Menu;
}
public override void CopyObserved(LocalPlayer observed)
{
OnChanged(this);
}
}
}

39
Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs


using System;
using UnityEngine;
namespace LobbyRelaySample
{
public class CallbackValue<T>
{
public Action<T> onChanged;
public CallbackValue()
{
}
public CallbackValue(T cachedValue)
{
m_CachedValue = cachedValue;
}
public T Value
{
get => m_CachedValue;
set
{
if (m_CachedValue.Equals(value))
return;
m_CachedValue = value;
onChanged?.Invoke(m_CachedValue);
}
}
public void SetNoCallback(T value)
{
m_CachedValue = value;
}
T m_CachedValue = default;
}
}

409
Assets/Scripts/GameLobby/Lobby/LobbyManager.cs


using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want. E.g. you can request to get a readable list of
/// current lobbies and not need to make the query call directly.
/// </summary>
///
/// Manages one Lobby at a time, Only entry points to a lobby with ID is via JoinAsync, CreateAsync, and QuickJoinAsync
public class LobbyManager : IDisposable
{
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the game will be actively in just one lobby at a time, though they could be in more on the service side.)
public Lobby CurrentLobby => m_CurrentLobby;
Lobby m_CurrentLobby;
const int k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
Task m_HeartBeatTask;
#region Rate Limiting
public enum RequestType
{
Query = 0,
Join,
QuickJoin,
Host
}
public bool InLobby()
{
if (m_CurrentLobby == null)
{
Debug.LogError("LobbyManager not currently in a lobby. Did you CreateLobbyAsync or JoinLobbyAsync?");
return false;
}
return true;
}
public RateLimiter GetRateLimit(RequestType type)
{
if (type == RequestType.Join)
return m_JoinCooldown;
else if (type == RequestType.QuickJoin)
return m_QuickJoinCooldown;
else if (type == RequestType.Host)
return m_CreateCooldown;
return m_QueryCooldown;
}
// Rate Limits are posted here: https://docs.unity.com/lobby/rate-limits.html
RateLimiter m_QueryCooldown = new RateLimiter(1f);
RateLimiter m_CreateCooldown = new RateLimiter(3f);
RateLimiter m_JoinCooldown = new RateLimiter(3f);
RateLimiter m_QuickJoinCooldown = new RateLimiter(10f);
RateLimiter m_GetLobbyCooldown = new RateLimiter(1f);
RateLimiter m_DeleteLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdateLobbyCooldown = new RateLimiter(.3f);
RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.3f);
RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.3f);
RateLimiter m_HeartBeatCooldown = new RateLimiter(6f);
#endregion
Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LocalPlayer user)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName.Value);
data.Add("DisplayName", displayNameObject);
return data;
}
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LocalPlayer localUser)
{
if (m_CreateCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit.");
return null;
}
await m_CreateCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Creating");
try
{
string uasId = AuthenticationService.Instance.PlayerId;
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
m_CurrentLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
StartHeartBeat();
return m_CurrentLobby;
}
catch (Exception ex)
{
Debug.LogError($"Lobby Create failed:\n{ex}");
return null;
}
}
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LocalPlayer localUser)
{
if (m_JoinCooldown.IsInCooldown ||
(lobbyId == null && lobbyCode == null))
{
return null;
}
await m_JoinCooldown.WaitUntilCooldown();
Debug.Log($"{localUser.DisplayName}({localUser.ID}) Joining Lobby- {lobbyId} with {lobbyCode}");
string uasId = AuthenticationService.Instance.PlayerId;
var playerData = CreateInitialPlayerData(localUser);
if (!string.IsNullOrEmpty(lobbyId))
{
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions
{ Player = new Player(id: uasId, data: playerData) };
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
}
else
{
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions
{ Player = new Player(id: uasId, data: playerData) };
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
}
return m_CurrentLobby;
}
public async Task<Lobby> QuickJoinLobbyAsync(LocalPlayer localUser, LobbyColor limitToColor = LobbyColor.None)
{
//We dont want to queue a quickjoin
if (m_QuickJoinCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit.");
return null;
}
await m_QuickJoinCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Quick Joining.");
var filters = LobbyColorToFilters(limitToColor);
string uasId = AuthenticationService.Instance.PlayerId;
var joinRequest = new QuickJoinLobbyOptions
{
Filter = filters,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
return m_CurrentLobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
}
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None)
{
await m_QueryCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Retrieving List.");
var filters = LobbyColorToFilters(limitToColor);
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Orange).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Green)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Green).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Blue)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Blue).ToString(),
QueryFilter.OpOptions.EQ));
return filters;
}
public async Task<Lobby> GetLobbyAsync(string lobbyId = null)
{
if (!InLobby())
return null;
await m_GetLobbyCooldown.WaitUntilCooldown();
lobbyId ??= m_CurrentLobby.Id;
return m_CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId);
}
public async Task LeaveLobbyAsync()
{
await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown();
if (!InLobby())
return;
string playerId = AuthenticationService.Instance.PlayerId;
Debug.Log($"{playerId} leaving Lobby {m_CurrentLobby.Id}");
await LobbyService.Instance.RemovePlayerAsync(m_CurrentLobby.Id, playerId);
m_CurrentLobby = null;
}
public async Task<Lobby> UpdatePlayerDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Player Data");
string playerId = AuthenticationService.Instance.PlayerId;
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member,
value: dataNew.Value);
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = dataCurr,
AllocationId = null,
ConnectionInfo = null
};
return m_CurrentLobby =
await LobbyService.Instance.UpdatePlayerAsync(m_CurrentLobby.Id, playerId, updateOptions);
}
public async Task<Lobby> UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Relay Info (Player)");
string playerId = AuthenticationService.Instance.PlayerId;
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = new Dictionary<string, PlayerDataObject>(),
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
return m_CurrentLobby = await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
public async Task<Lobby> UpdateLobbyDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdateLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Lobby Data");
Dictionary<string, DataObject> dataCurr = m_CurrentLobby.Data ?? new Dictionary<string, DataObject>();
var shouldLock = false;
foreach (var dataNew in data)
{
// Special case: We want to be able to filter on our color data, so we need to supply an arbitrary index to retrieve later. Uses N# for numerics, instead of S# for strings.
DataObject.IndexOptions index = dataNew.Key == "LocalLobbyColor" ? DataObject.IndexOptions.N1 : 0;
DataObject
dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value,
index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
//Special Use: Get the state of the Local lobby so we can lock it from appearing in queries if it's not in the "Lobby" LocalLobbyState
if (dataNew.Key == "LocalLobbyState")
{
Enum.TryParse(dataNew.Value, out LobbyState lobbyState);
shouldLock = lobbyState != LobbyState.Lobby;
}
}
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = dataCurr, IsLocked = shouldLock };
return m_CurrentLobby = await LobbyService.Instance.UpdateLobbyAsync(m_CurrentLobby.Id, updateOptions);
}
public async Task DeleteLobbyAsync()
{
if (!InLobby())
return;
await m_DeleteLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Deleting Lobby");
await LobbyService.Instance.DeleteLobbyAsync(m_CurrentLobby.Id);
}
public void Dispose()
{
m_CurrentLobby = null;
m_HeartBeatTask.Dispose();
}
#region HeartBeat
//Since the LobbyManager maintains the "connection" to the lobby, we will continue to heartbeat until host leaves.
async Task SendHeartbeatPingAsync()
{
if (!InLobby())
return;
if (m_HeartBeatCooldown.IsInCooldown)
return;
await m_HeartBeatCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Heartbeat");
await LobbyService.Instance.SendHeartbeatPingAsync(m_CurrentLobby.Id);
}
void StartHeartBeat()
{
#pragma warning disable 4014
m_HeartBeatTask = HeartBeatLoop();
#pragma warning restore 4014
}
async Task HeartBeatLoop()
{
while (m_CurrentLobby != null)
{
await SendHeartbeatPingAsync();
await Task.Delay(8000);
}
}
#endregion
}
//Manages the Cooldown for each service call.
//Adds a buffer to account for ping times.
public class RateLimiter
{
public Action<bool> onCooldownChange;
public readonly float cooldownSeconds;
public readonly int coolDownMS;
public readonly int pingBufferMS;
//(If you're still getting rate limit errors, try increasing the pingBuffer)
public RateLimiter(float cooldownSeconds, int pingBuffer = 100)
{
this.cooldownSeconds = cooldownSeconds;
pingBufferMS = pingBuffer;
coolDownMS =
Mathf.CeilToInt(this.cooldownSeconds * 1000) +
pingBufferMS;
}
public async Task WaitUntilCooldown()
{
//No Queue!
if (!m_IsInCooldown)
{
#pragma warning disable 4014
CooldownAsync();
#pragma warning restore 4014
return;
}
while (m_IsInCooldown)
{
await Task.Delay(10);
}
}
async Task CooldownAsync()
{
IsInCooldown = true;
await Task.Delay(coolDownMS);
IsInCooldown = false;
}
bool m_IsInCooldown = false;
public bool IsInCooldown
{
get => m_IsInCooldown;
private set
{
if (m_IsInCooldown != value)
{
m_IsInCooldown = value;
onCooldownChange?.Invoke(m_IsInCooldown);
}
}
}
}
}

193
Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs


using System;
using System.Threading.Tasks;
using LobbyRelaySample.lobby;
using Unity.Services.Lobbies.Models;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbySynchronizer : IReceiveMessages, IDisposable
{
LocalLobby m_LocalLobby;
LocalPlayer m_LocalUser;
LobbyManager m_LobbyManager;
bool m_LocalChanges = false;
const int
k_approvalMaxMS = 10000; // Used for determining if a user should timeout if they are unable to connect.
int m_lifetime = 0;
const int k_UpdateIntervalMS = 1000;
public LobbySynchronizer(LobbyManager lobbyManager)
{
m_LobbyManager = lobbyManager;
}
public void StartSynch(LocalLobby localLobby, LocalPlayer localUser)
{
m_LocalUser = localUser;
m_LocalLobby = localLobby;
m_LocalLobby.LobbyID.onChanged += OnLobbyIdChanged;
m_LocalChanges = true;
Locator.Get.Messenger.Subscribe(this);
#pragma warning disable 4014
UpdateLoopAsync();
#pragma warning restore 4014
m_lifetime = 0;
}
public void EndSynch()
{
m_LocalChanges = false;
Locator.Get.Messenger.Unsubscribe(this);
if (m_LocalLobby != null)
m_LocalLobby.LobbyID.onChanged -= OnLobbyIdChanged;
m_LocalLobby = null;
}
//TODO Stop players from joining lobby while game is underway.
public void OnReceiveMessage(MessageType type, object msg)
{
// if (type == MessageType.ClientUserSeekingDisapproval)
// {
// bool shouldDisapprove =
// m_LocalLobby.LocalLobbyState !=
// LocalLobbyState.Lobby; // By not refreshing, it's possible to have a lobby in the lobby list UI after its countdown starts and then try joining.
// if (shouldDisapprove)
// (msg as Action<relay.Approval>)?.Invoke(relay.Approval.GameAlreadyStarted);
// }
}
/// <summary>
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
/// (Unless we're already awaiting a query, in which case continue waiting.)
/// </summary>
async Task UpdateLoopAsync()
{
Lobby latestLobby = null;
while (m_LocalLobby != null)
{
latestLobby = await GetLatestRemoteLobby();
if (IfRemoteLobbyChanged(latestLobby))
{
//Pulling remote changes, and applying them to the local lobby usually flags it as changed,
//Causing another pull, the RemoteToLocal converter ensures this does not happen by flagging the lobby.
LobbyConverters.RemoteToLocal(latestLobby, m_LocalLobby, false);
}
if (!LobbyHasHost())
{
LeaveLobbyBecauseNoHost();
break;
}
var areAllusersReady = AreAllUsersReady();
if (areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.Lobby)
{
GameManager.Instance.BeginCountdown();
}
else if (!areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.CountDown)
{
GameManager.Instance.CancelCountDown();
}
m_lifetime += k_UpdateIntervalMS;
await Task.Delay(k_UpdateIntervalMS);
}
}
async Task<Lobby> GetLatestRemoteLobby()
{
Lobby latestLobby = null;
if (m_LocalLobby.IsLobbyChanged())
{
latestLobby = await PushDataToLobby();
}
else
{
latestLobby = await m_LobbyManager.GetLobbyAsync();
}
return latestLobby;
}
bool IfRemoteLobbyChanged(Lobby remoteLobby)
{
var remoteLobbyTime = remoteLobby.LastUpdated.ToFileTimeUtc();
var localLobbyTime = m_LocalLobby.LastUpdated.Value;
var isLocalOutOfDate = remoteLobbyTime > localLobbyTime;
return isLocalOutOfDate;
}
async Task<Lobby> PushDataToLobby()
{
m_LocalChanges = false;
if (m_LocalUser.IsHost.Value)
await m_LobbyManager.UpdateLobbyDataAsync(
LobbyConverters.LocalToRemoteData(m_LocalLobby));
return await m_LobbyManager.UpdatePlayerDataAsync(
LobbyConverters.LocalToRemoteUserData(m_LocalUser));
}
bool AreAllUsersReady()
{
foreach (var lobbyUser in m_LocalLobby.LocalPlayers.Values)
{
if (lobbyUser.UserStatus.Value != UserStatus.Ready)
{
return false;
}
}
return true;
}
bool LobbyHasHost()
{
if (!m_LocalUser.IsHost.Value)
{
foreach (var lobbyUser in m_LocalLobby.LocalPlayers)
{
if (lobbyUser.Value.IsHost.Value)
return true;
}
return false;
}
return true;
}
void LeaveLobbyBecauseNoHost()
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
public void OnLobbyIdChanged(string lobbyID)
{
if (string.IsNullOrEmpty(lobbyID)
) // When the player leaves, their LocalLobby is cleared out.
{
EndSynch();
}
}
public void Dispose()
{
EndSynch();
}
}
}

3
Packages/ParrelSync.meta


fileFormatVersion: 2
guid: 520a1688a3c84604b0e7836ecc15cee3
timeCreated: 1654698051

167
ProjectSettings/SceneTemplateSettings.json


{
"templatePinStates": [],
"dependencyTypeInfos": [
{
"userAdded": false,
"type": "UnityEngine.AnimationClip",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEditor.Animations.AnimatorController",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.AnimatorOverrideController",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEditor.Audio.AudioMixerController",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.ComputeShader",
"ignore": true,
"defaultInstantiationMode": 1,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Cubemap",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.GameObject",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEditor.LightingDataAsset",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": false
},
{
"userAdded": false,
"type": "UnityEngine.LightingSettings",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Material",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEditor.MonoScript",
"ignore": true,
"defaultInstantiationMode": 1,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.PhysicMaterial",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.PhysicsMaterial2D",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.PostProcessing.PostProcessResources",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.VolumeProfile",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEditor.SceneAsset",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": false
},
{
"userAdded": false,
"type": "UnityEngine.Shader",
"ignore": true,
"defaultInstantiationMode": 1,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.ShaderVariantCollection",
"ignore": true,
"defaultInstantiationMode": 1,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Texture",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Texture2D",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
},
{
"userAdded": false,
"type": "UnityEngine.Timeline.TimelineAsset",
"ignore": false,
"defaultInstantiationMode": 0,
"supportsModification": true
}
],
"defaultDependencyTypeInfo": {
"userAdded": false,
"type": "<default_scene_template_dependencies>",
"ignore": false,
"defaultInstantiationMode": 1,
"supportsModification": true
},
"newSceneOverride": 0
}

8
Packages/ParrelSync/Editor.meta


fileFormatVersion: 2
guid: a31ea7d0315594440839cdb0db6bc411
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

10
Packages/ParrelSync/package.json


{
"name": "com.veriorpies.parrelsync",
"displayName": "ParrelSync",
"version": "1.5.0",
"unity": "2018.4",
"description": "ParrelSync is a Unity editor extension that allows users to test multiplayer gameplay without building the project by having another Unity editor window opened and mirror the changes from the original project.",
"license": "MIT",
"keywords": [ "Networking", "Utils", "Editor", "Extensions" ],
"dependencies": {}
}

7
Packages/ParrelSync/package.json.meta


fileFormatVersion: 2
guid: a2a889c264e34b47a7349cbcb2cbedd7
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

15
Packages/ParrelSync/projectCloner.asmdef


{
"name": "ParrelSync",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

7
Packages/ParrelSync/projectCloner.asmdef.meta


fileFormatVersion: 2
guid: 894a6cc6ed5cd2645bb542978cbed6a9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Packages/ParrelSync/Editor/AssetModBlock.meta


fileFormatVersion: 2
guid: 8b14e706b1e7cb044b23837e8a70cad9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

22
Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs


using UnityEditor;
namespace ParrelSync
{
[InitializeOnLoad]
public class EditorQuit
{
/// <summary>
/// Is editor being closed
/// </summary>
static public bool IsQuiting { get; private set; }
static void Quit()
{
IsQuiting = true;
}
static EditorQuit()
{
IsQuiting = false;
EditorApplication.quitting += Quit;
}
}
}

11
Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta


fileFormatVersion: 2
guid: bf2888ff90706904abc2d851c3e59e00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

34
Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs


using UnityEditor;
using UnityEngine;
namespace ParrelSync
{
/// <summary>
/// For preventing assets being modified from the clone instance.
/// </summary>
public class ParrelSyncAssetModificationProcessor : UnityEditor.AssetModificationProcessor
{
public static string[] OnWillSaveAssets(string[] paths)
{
if (ClonesManager.IsClone() && Preferences.AssetModPref.Value)
{
if (paths != null && paths.Length > 0 && !EditorQuit.IsQuiting)
{
EditorUtility.DisplayDialog(
ClonesManager.ProjectName + ": Asset modifications saving detected and blocked",
"Asset modifications saving are blocked in the clone instance. \n\n" +
"This is a clone of the original project. \n" +
"Making changes to asset files via the clone editor is not recommended. \n" +
"Please use the original editor window if you want to make changes to the project files.",
"ok"
);
foreach (var path in paths)
{
Debug.Log("Attempting to save " + path + " are blocked.");
}
}
return new string[0] { };
}
return paths;
}
}
}

11
Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta


fileFormatVersion: 2
guid: 755e570bd21b39440a923056e60f1450
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

664
Packages/ParrelSync/Editor/ClonesManager.cs


using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;
using Debug = UnityEngine.Debug;
namespace ParrelSync
{
/// <summary>
/// Contains all required methods for creating a linked clone of the Unity project.
/// </summary>
public class ClonesManager
{
/// <summary>
/// Name used for an identifying file created in the clone project directory.
/// </summary>
/// <remarks>
/// (!) Do not change this after the clone was created, because then connection will be lost.
/// </remarks>
public const string CloneFileName = ".clone";
/// <summary>
/// Suffix added to the end of the project clone name when it is created.
/// </summary>
/// <remarks>
/// (!) Do not change this after the clone was created, because then connection will be lost.
/// </remarks>
public const string CloneNameSuffix = "_clone";
public const string ProjectName = "ParrelSync";
/// <summary>
/// The maximum number of clones
/// </summary>
public const int MaxCloneProjectCount = 10;
/// <summary>
/// Name of the file for storing clone's argument.
/// </summary>
public const string ArgumentFileName = ".parrelsyncarg";
/// <summary>
/// Default argument of the new clone
/// </summary>
public const string DefaultArgument = "client";
#region Managing clones
/// <summary>
/// Creates clone from the project currently open in Unity Editor.
/// </summary>
/// <returns></returns>
public static Project CreateCloneFromCurrent()
{
if (IsClone())
{
Debug.LogError("This project is already a clone. Cannot clone it.");
return null;
}
string currentProjectPath = ClonesManager.GetCurrentProjectPath();
return ClonesManager.CreateCloneFromPath(currentProjectPath);
}
/// <summary>
/// Creates clone of the project located at the given path.
/// </summary>
/// <param name="sourceProjectPath"></param>
/// <returns></returns>
public static Project CreateCloneFromPath(string sourceProjectPath)
{
Project sourceProject = new Project(sourceProjectPath);
string cloneProjectPath = null;
//Find available clone suffix id
for (int i = 0; i < MaxCloneProjectCount; i++)
{
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath;
string possibleCloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i;
if (!Directory.Exists(possibleCloneProjectPath))
{
cloneProjectPath = possibleCloneProjectPath;
break;
}
}
if (string.IsNullOrEmpty(cloneProjectPath))
{
Debug.LogError("The number of cloned projects has reach its limit. Limit: " + MaxCloneProjectCount);
return null;
}
Project cloneProject = new Project(cloneProjectPath);
Debug.Log("Start cloning project, original project: " + sourceProject + ", clone project: " + cloneProject);
ClonesManager.CreateProjectFolder(cloneProject);
//Copy Folders
Debug.Log("Library copy: " + cloneProject.libraryPath);
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, cloneProject.libraryPath,
"Cloning Project Library '" + sourceProject.name + "'. ");
Debug.Log("Packages copy: " + cloneProject.libraryPath);
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.packagesPath, cloneProject.packagesPath,
"Cloning Project Packages '" + sourceProject.name + "'. ");
//Link Folders
ClonesManager.LinkFolders(sourceProject.assetPath, cloneProject.assetPath);
ClonesManager.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath);
ClonesManager.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath);
ClonesManager.LinkFolders(sourceProject.localPackages, cloneProject.localPackages);
ClonesManager.RegisterClone(cloneProject);
return cloneProject;
}
/// <summary>
/// Registers a clone by placing an identifying ".clone" file in its root directory.
/// </summary>
/// <param name="cloneProject"></param>
private static void RegisterClone(Project cloneProject)
{
/// Add clone identifier file.
string identifierFile = Path.Combine(cloneProject.projectPath, ClonesManager.CloneFileName);
File.Create(identifierFile).Dispose();
//Add argument file with default argument
string argumentFilePath = Path.Combine(cloneProject.projectPath, ClonesManager.ArgumentFileName);
File.WriteAllText(argumentFilePath, DefaultArgument, System.Text.Encoding.UTF8);
/// Add collabignore.txt to stop the clone from messing with Unity Collaborate if it's enabled. Just in case.
string collabignoreFile = Path.Combine(cloneProject.projectPath, "collabignore.txt");
File.WriteAllText(collabignoreFile, "*"); /// Make it ignore ALL files in the clone.
}
/// <summary>
/// Opens a project located at the given path (if one exists).
/// </summary>
/// <param name="projectPath"></param>
public static void OpenProject(string projectPath)
{
if (!Directory.Exists(projectPath))
{
Debug.LogError("Cannot open the project - provided folder (" + projectPath + ") does not exist.");
return;
}
if (projectPath == ClonesManager.GetCurrentProjectPath())
{
Debug.LogError("Cannot open the project - it is already open.");
return;
}
string fileName = GetApplicationPath();
string args = "-projectPath \"" + projectPath + "\"";
Debug.Log("Opening project \"" + fileName + " " + args + "\"");
ClonesManager.StartHiddenConsoleProcess(fileName, args);
}
private static string GetApplicationPath()
{
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
return EditorApplication.applicationPath;
case RuntimePlatform.OSXEditor:
return EditorApplication.applicationPath + "/Contents/MacOS/Unity";
case RuntimePlatform.LinuxEditor:
return EditorApplication.applicationPath;
default:
throw new System.NotImplementedException("Platform has not supported yet ;(");
}
}
/// <summary>
/// Is this project being opened by an Unity editor?
/// </summary>
/// <param name="projectPath"></param>
/// <returns></returns>
public static bool IsCloneProjectRunning(string projectPath)
{
//Determine whether it is opened in another instance by checking the UnityLockFile
string UnityLockFilePath = new string[] { projectPath, "Temp", "UnityLockfile" }
.Aggregate(Path.Combine);
switch (Application.platform)
{
case (RuntimePlatform.WindowsEditor):
//Windows editor will lock "UnityLockfile" file when project is being opened.
//Sometime, for instance: windows editor crash, the "UnityLockfile" will not be deleted even the project
//isn't being opened, so a check to the "UnityLockfile" lock status may be necessary.
if (Preferences.AlsoCheckUnityLockFileStaPref.Value)
return File.Exists(UnityLockFilePath) && FileUtilities.IsFileLocked(UnityLockFilePath);
else
return File.Exists(UnityLockFilePath);
case (RuntimePlatform.OSXEditor):
//Mac editor won't lock "UnityLockfile" file when project is being opened
return File.Exists(UnityLockFilePath);
case (RuntimePlatform.LinuxEditor):
return File.Exists(UnityLockFilePath);
default:
throw new System.NotImplementedException("IsCloneProjectRunning: Unsupport Platfrom: " + Application.platform);
}
}
/// <summary>
/// Deletes the clone of the currently open project, if such exists.
/// </summary>
public static void DeleteClone(string cloneProjectPath)
{
/// Clone won't be able to delete itself.
if (ClonesManager.IsClone()) return;
///Extra precautions.
if (cloneProjectPath == string.Empty) return;
if (cloneProjectPath == ClonesManager.GetOriginalProjectPath()) return;
//Check what OS is
string identifierFile;
string args;
switch (Application.platform)
{
case (RuntimePlatform.WindowsEditor):
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
File.Delete(identifierFile);
args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath);
StartHiddenConsoleProcess("cmd.exe", args);
break;
case (RuntimePlatform.OSXEditor):
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
File.Delete(identifierFile);
FileUtil.DeleteFileOrDirectory(cloneProjectPath);
break;
case (RuntimePlatform.LinuxEditor):
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
File.Delete(identifierFile);
FileUtil.DeleteFileOrDirectory(cloneProjectPath);
break;
default:
Debug.LogWarning("Not in a known editor. Where are you!?");
break;
}
}
#endregion
#region Creating project folders
/// <summary>
/// Creates an empty folder using data in the given Project object
/// </summary>
/// <param name="project"></param>
public static void CreateProjectFolder(Project project)
{
string path = project.projectPath;
Debug.Log("Creating new empty folder at: " + path);
Directory.CreateDirectory(path);
}
/// <summary>
/// Copies the full contents of the unity library. We want to do this to avoid the lengthy re-serialization of the whole project when it opens up the clone.
/// </summary>
/// <param name="sourceProject"></param>
/// <param name="destinationProject"></param>
[System.Obsolete]
public static void CopyLibraryFolder(Project sourceProject, Project destinationProject)
{
if (Directory.Exists(destinationProject.libraryPath))
{
Debug.LogWarning("Library copy: destination path already exists! ");
return;
}
Debug.Log("Library copy: " + destinationProject.libraryPath);
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, destinationProject.libraryPath,
"Cloning project '" + sourceProject.name + "'. ");
}
#endregion
#region Creating symlinks
/// <summary>
/// Creates a symlink between destinationPath and sourcePath (Mac version).
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
private static void CreateLinkMac(string sourcePath, string destinationPath)
{
sourcePath = sourcePath.Replace(" ", "\\ ");
destinationPath = destinationPath.Replace(" ", "\\ ");
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);
Debug.Log("Mac hard link " + command);
ClonesManager.ExecuteBashCommand(command);
}
/// <summary>
/// Creates a symlink between destinationPath and sourcePath (Linux version).
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
private static void CreateLinkLinux(string sourcePath, string destinationPath)
{
sourcePath = sourcePath.Replace(" ", "\\ ");
destinationPath = destinationPath.Replace(" ", "\\ ");
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);
Debug.Log("Linux Symlink " + command);
ClonesManager.ExecuteBashCommand(command);
}
/// <summary>
/// Creates a symlink between destinationPath and sourcePath (Windows version).
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
private static void CreateLinkWin(string sourcePath, string destinationPath)
{
string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath);
Debug.Log("Windows junction: " + cmd);
ClonesManager.StartHiddenConsoleProcess("cmd.exe", cmd);
}
//TODO(?) avoid terminal calls and use proper api stuff. See below for windows!
////https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol
//[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
//private static extern bool DeviceIoControl(System.IntPtr hDevice, uint dwIoControlCode,
// System.IntPtr InBuffer, int nInBufferSize,
// System.IntPtr OutBuffer, int nOutBufferSize,
// out int pBytesReturned, System.IntPtr lpOverlapped);
/// <summary>
/// Create a link / junction from the original project to it's clone.
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
public static void LinkFolders(string sourcePath, string destinationPath)
{
if ((Directory.Exists(destinationPath) == false) && (Directory.Exists(sourcePath) == true))
{
switch (Application.platform)
{
case (RuntimePlatform.WindowsEditor):
CreateLinkWin(sourcePath, destinationPath);
break;
case (RuntimePlatform.OSXEditor):
CreateLinkMac(sourcePath, destinationPath);
break;
case (RuntimePlatform.LinuxEditor):
CreateLinkLinux(sourcePath, destinationPath);
break;
default:
Debug.LogWarning("Not in a known editor. Application.platform: " + Application.platform);
break;
}
}
else
{
Debug.LogWarning("Skipping Asset link, it already exists: " + destinationPath);
}
}
#endregion
#region Utility methods
private static bool? isCloneFileExistCache = null;
/// <summary>
/// Returns true if the project currently open in Unity Editor is a clone.
/// </summary>
/// <returns></returns>
public static bool IsClone()
{
if (isCloneFileExistCache == null)
{
/// The project is a clone if its root directory contains an empty file named ".clone".
string cloneFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.CloneFileName);
isCloneFileExistCache = File.Exists(cloneFilePath);
}
return (bool)isCloneFileExistCache;
}
/// <summary>
/// Get the path to the current unityEditor project folder's info
/// </summary>
/// <returns></returns>
public static string GetCurrentProjectPath()
{
return Application.dataPath.Replace("/Assets", "");
}
/// <summary>
/// Return a project object that describes all the paths we need to clone it.
/// </summary>
/// <returns></returns>
public static Project GetCurrentProject()
{
string pathString = ClonesManager.GetCurrentProjectPath();
return new Project(pathString);
}
/// <summary>
/// Get the argument of this clone project.
/// If this is the original project, will return an empty string.
/// </summary>
/// <returns></returns>
public static string GetArgument()
{
string argument = "";
if (IsClone())
{
string argumentFilePath = Path.Combine(GetCurrentProjectPath(), ClonesManager.ArgumentFileName);
if (File.Exists(argumentFilePath))
{
argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
}
}
return argument;
}
/// <summary>
/// Returns the path to the original project.
/// If currently open project is the original, returns its own path.
/// If the original project folder cannot be found, retuns an empty string.
/// </summary>
/// <returns></returns>
public static string GetOriginalProjectPath()
{
if (IsClone())
{
/// If this is a clone...
/// Original project path can be deduced by removing the suffix from the clone's path.
string cloneProjectPath = ClonesManager.GetCurrentProject().projectPath;
int index = cloneProjectPath.LastIndexOf(ClonesManager.CloneNameSuffix);
if (index > 0)
{
string originalProjectPath = cloneProjectPath.Substring(0, index);
if (Directory.Exists(originalProjectPath)) return originalProjectPath;
}
return string.Empty;
}
else
{
/// If this is the original, we return its own path.
return ClonesManager.GetCurrentProjectPath();
}
}
/// <summary>
/// Returns all clone projects path.
/// </summary>
/// <returns></returns>
public static List<string> GetCloneProjectsPath()
{
List<string> projectsPath = new List<string>();
for (int i = 0; i < MaxCloneProjectCount; i++)
{
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath;
string cloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i;
if (Directory.Exists(cloneProjectPath))
projectsPath.Add(cloneProjectPath);
}
return projectsPath;
}
/// <summary>
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
/// </summary>
/// <param name="source">Directory to be copied.</param>
/// <param name="destination">Destination directory (created automatically if needed).</param>
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
public static void CopyDirectoryWithProgressBar(string sourcePath, string destinationPath,
string progressBarPrefix = "")
{
var source = new DirectoryInfo(sourcePath);
var destination = new DirectoryInfo(destinationPath);
long totalBytes = 0;
long copiedBytes = 0;
ClonesManager.CopyDirectoryWithProgressBarRecursive(source, destination, ref totalBytes, ref copiedBytes,
progressBarPrefix);
EditorUtility.ClearProgressBar();
}
/// <summary>
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
/// Same as the previous method, but uses recursion to copy all nested folders as well.
/// </summary>
/// <param name="source">Directory to be copied.</param>
/// <param name="destination">Destination directory (created automatically if needed).</param>
/// <param name="totalBytes">Total bytes to be copied. Calculated automatically, initialize at 0.</param>
/// <param name="copiedBytes">To track already copied bytes. Calculated automatically, initialize at 0.</param>
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
private static void CopyDirectoryWithProgressBarRecursive(DirectoryInfo source, DirectoryInfo destination,
ref long totalBytes, ref long copiedBytes, string progressBarPrefix = "")
{
/// Directory cannot be copied into itself.
if (source.FullName.ToLower() == destination.FullName.ToLower())
{
Debug.LogError("Cannot copy directory into itself.");
return;
}
/// Calculate total bytes, if required.
if (totalBytes == 0)
{
totalBytes = ClonesManager.GetDirectorySize(source, true, progressBarPrefix);
}
/// Create destination directory, if required.
if (!Directory.Exists(destination.FullName))
{
Directory.CreateDirectory(destination.FullName);
}
/// Copy all files from the source.
foreach (FileInfo file in source.GetFiles())
{
try
{
file.CopyTo(Path.Combine(destination.ToString(), file.Name), true);
}
catch (IOException)
{
/// Some files may throw IOException if they are currently open in Unity editor.
/// Just ignore them in such case.
}
/// Account the copied file size.
copiedBytes += file.Length;
/// Display the progress bar.
float progress = (float)copiedBytes / (float)totalBytes;
bool cancelCopy = EditorUtility.DisplayCancelableProgressBar(
progressBarPrefix + "Copying '" + source.FullName + "' to '" + destination.FullName + "'...",
"(" + (progress * 100f).ToString("F2") + "%) Copying file '" + file.Name + "'...",
progress);
if (cancelCopy) return;
}
/// Copy all nested directories from the source.
foreach (DirectoryInfo sourceNestedDir in source.GetDirectories())
{
DirectoryInfo nextDestingationNestedDir = destination.CreateSubdirectory(sourceNestedDir.Name);
ClonesManager.CopyDirectoryWithProgressBarRecursive(sourceNestedDir, nextDestingationNestedDir,
ref totalBytes, ref copiedBytes, progressBarPrefix);
}
}
/// <summary>
/// Calculates the size of the given directory. Displays a progress bar.
/// </summary>
/// <param name="directory">Directory, which size has to be calculated.</param>
/// <param name="includeNested">If true, size will include all nested directories.</param>
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
/// <returns>Size of the directory in bytes.</returns>
private static long GetDirectorySize(DirectoryInfo directory, bool includeNested = false,
string progressBarPrefix = "")
{
EditorUtility.DisplayProgressBar(progressBarPrefix + "Calculating size of directories...",
"Scanning '" + directory.FullName + "'...", 0f);
/// Calculate size of all files in directory.
long filesSize = directory.GetFiles().Sum((FileInfo file) => file.Length);
/// Calculate size of all nested directories.
long directoriesSize = 0;
if (includeNested)
{
IEnumerable<DirectoryInfo> nestedDirectories = directory.GetDirectories();
foreach (DirectoryInfo nestedDir in nestedDirectories)
{
directoriesSize += ClonesManager.GetDirectorySize(nestedDir, true, progressBarPrefix);
}
}
return filesSize + directoriesSize;
}
/// <summary>
/// Starts process in the system console, taking the given fileName and args.
/// </summary>
/// <param name="fileName"></param>
/// <param name="args"></param>
private static void StartHiddenConsoleProcess(string fileName, string args)
{
System.Diagnostics.Process.Start(fileName, args);
}
/// <summary>
/// Thanks to https://github.com/karl-/unity-symlink-utility/blob/master/SymlinkUtility.cs
/// </summary>
/// <param name="command"></param>
private static void ExecuteBashCommand(string command)
{
command = command.Replace("\"", "\"\"");
var proc = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = "-c \"" + command + "\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
using (proc)
{
proc.Start();
proc.WaitForExit();
if (!proc.StandardError.EndOfStream)
{
UnityEngine.Debug.LogError(proc.StandardError.ReadToEnd());
}
}
}
public static void OpenProjectInFileExplorer(string path)
{
System.Diagnostics.Process.Start(@path);
}
#endregion
}
}

11
Packages/ParrelSync/Editor/ClonesManager.cs.meta


fileFormatVersion: 2
guid: 6148e48ed6b61d748b187d06d3687b83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Packages/ParrelSync/Editor/ClonesManagerWindow.cs.meta


fileFormatVersion: 2
guid: a041d83486c20b84bbf5077ddfbbca37
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

13
Packages/ParrelSync/Editor/ExternalLinks.cs


namespace ParrelSync
{
public class ExternalLinks
{
public const string RemoteVersionURL = "https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/VERSION.txt";
public const string Releases = "https://github.com/VeriorPies/ParrelSync/releases";
public const string CustomArgumentHelpLink = "https://github.com/VeriorPies/ParrelSync/wiki/Argument";
public const string GitHubHome = "https://github.com/VeriorPies/ParrelSync/";
public const string GitHubIssue = "https://github.com/VeriorPies/ParrelSync/issues";
public const string FAQ = "https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs";
}
}

11
Packages/ParrelSync/Editor/ExternalLinks.cs.meta


fileFormatVersion: 2
guid: 65daf17fbe5101b41977305639f30c65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

部分文件因为文件数量过多而无法显示

正在加载...
取消
保存