浏览代码

Merge pull request #10 from Unity-Technologies/master

plastic-dev-master to github main
/main
GitHub 3 年前
当前提交
ab507ae5
共有 321 个文件被更改,包括 10610 次插入1334 次删除
  1. 19
      Assets/Prefabs/UI/CreateContent.prefab
  2. 161
      Assets/Prefabs/UI/GameCanvas.prefab
  3. 893
      Assets/Prefabs/UI/JoinContent.prefab
  4. 91
      Assets/Prefabs/UI/JoinCreateCanvas.prefab
  5. 32
      Assets/Prefabs/UI/LobbyButtonUI.prefab
  6. 14
      Assets/Prefabs/UI/LobbyCodeCanvas.prefab
  7. 560
      Assets/Prefabs/UI/LobbyGameCanvas.prefab
  8. 130
      Assets/Prefabs/UI/LobbyUserList.prefab
  9. 868
      Assets/Prefabs/UI/PlayerInteractionPanel.prefab
  10. 30
      Assets/Prefabs/UI/RelayCodeCanvas.prefab
  11. 19
      Assets/Prefabs/UI/RenamePopup.prefab
  12. 174
      Assets/Prefabs/UI/UserCardPanel.prefab
  13. 2
      Assets/Prefabs/GameManager.prefab
  14. 767
      Assets/Scenes/mainScene.unity
  15. 12
      Assets/Scripts/Auth/Identity.cs
  16. 6
      Assets/Scripts/Auth/NameGenerator.cs
  17. 4
      Assets/Scripts/Auth/SubIdentity_Authentication.cs
  18. 48
      Assets/Scripts/Infrastructure/Locator.cs
  19. 48
      Assets/Scripts/Infrastructure/LogHandler.cs
  20. 84
      Assets/Scripts/Infrastructure/Messenger.cs
  21. 7
      Assets/Scripts/Infrastructure/Observed.cs
  22. 9
      Assets/Scripts/Infrastructure/ObserverBehaviour.cs
  23. 163
      Assets/Scripts/Infrastructure/UpdateSlow.cs
  24. 118
      Assets/Scripts/Lobby/LobbyAPIInterface.cs
  25. 248
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  26. 56
      Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
  27. 67
      Assets/Scripts/Lobby/ToLocalLobby.cs
  28. 5
      Assets/Scripts/LobbyRelaySample.asmdef
  29. 3
      Assets/Scripts/Tests/Editor/AuthTests.cs
  30. 38
      Assets/Scripts/Tests/Editor/LoggerTests.cs
  31. 53
      Assets/Scripts/Tests/Editor/MessengerTests.cs
  32. 22
      Assets/Scripts/Tests/Editor/ObserverTests.cs
  33. 80
      Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs
  34. 99
      Assets/Scripts/Tests/PlayMode/RelayRoundTripTests.cs
  35. 112
      Assets/Scripts/Tests/PlayMode/UpdateSlowTests.cs
  36. 12
      Assets/Scripts/UI/CountdownUI.cs
  37. 4
      Assets/Scripts/UI/EmoteButtonUI.cs
  38. 2
      Assets/Scripts/UI/EndGameButtonUI.cs
  39. 19
      Assets/Scripts/UI/InLobbyUserUI.cs
  40. 1
      Assets/Scripts/UI/JoinCreateLobbyUI.cs
  41. 24
      Assets/Scripts/UI/JoinMenuUI.cs
  42. 2
      Assets/Scripts/UI/ReadyCheckUI.cs
  43. 15
      Assets/Scripts/UI/SpinnerUI.cs
  44. 24
      Assets/Scripts/UI/UIPanelBase.cs
  45. 5
      Assets/Scripts/Game/ServerAddress.cs
  46. 136
      Assets/Scripts/Game/LocalLobby.cs
  47. 15
      Assets/Scripts/Game/LocalGameState.cs
  48. 124
      Assets/Scripts/Game/LobbyUser.cs
  49. 14
      Assets/Scripts/Game/LobbyServiceData.cs
  50. 19
      Packages/manifest.json
  51. 164
      Packages/packages-lock.json
  52. 22
      ProjectSettings/PackageManagerSettings.asset
  53. 20
      ProjectSettings/ProjectSettings.asset
  54. 4
      ProjectSettings/ProjectVersion.txt
  55. 2
      ProjectSettings/UnityConnectSettings.asset
  56. 157
      README.md
  57. 2
      Assets/Scripts/UI/RateLimitVisibility.cs.meta
  58. 8
      Assets/Art/Crown.meta
  59. 1001
      Assets/Prefabs/UI/PopUpUI.prefab
  60. 7
      Assets/Prefabs/UI/PopUpUI.prefab.meta
  61. 44
      Assets/Scripts/Infrastructure/AsyncRequest.cs
  62. 11
      Assets/Scripts/Infrastructure/AsyncRequest.cs.meta
  63. 32
      Assets/Scripts/Infrastructure/LogHandlerSettings.cs
  64. 11
      Assets/Scripts/Infrastructure/LogHandlerSettings.cs.meta
  65. 66
      Assets/Scripts/Relay/RelayAPIInterface.cs
  66. 243
      Assets/Scripts/Relay/RelayUtpClient.cs
  67. 11
      Assets/Scripts/Relay/RelayUtpClient.cs.meta
  68. 146
      Assets/Scripts/Relay/RelayUtpHost.cs
  69. 11
      Assets/Scripts/Relay/RelayUtpHost.cs.meta
  70. 225
      Assets/Scripts/Relay/RelayUtpSetup.cs
  71. 11
      Assets/Scripts/Relay/RelayUtpSetup.cs.meta
  72. 25
      Assets/Scripts/UI/PopUpUI.cs
  73. 11
      Assets/Scripts/UI/PopUpUI.cs.meta
  74. 35
      Assets/Scripts/UI/RateLimitVisibility.cs
  75. 39
      Assets/Scripts/UI/RecolorForLobbyType.cs
  76. 11
      Assets/Scripts/UI/RecolorForLobbyType.cs.meta
  77. 17
      Assets/Scripts/Game/EmoteType.cs
  78. 11
      Assets/Scripts/Game/EmoteType.cs.meta
  79. 331
      Assets/Scripts/Game/GameManager.cs
  80. 16
      ProjectSettings/BurstAotSettings_StandaloneWindows.json
  81. 6
      ProjectSettings/CommonBurstAotSettings.json
  82. 1001
      README.pdf
  83. 1001
      ~Documentation/Images/1_lobby_list.PNG
  84. 1001
      ~Documentation/Images/2_lobby.PNG
  85. 119
      Assets/Art/Crown/Crown.png
  86. 96
      Assets/Art/Crown/Crown.png.meta
  87. 90
      Packages/com.unity.transport/.gitattributes
  88. 3
      Packages/com.unity.transport/.gitmodules
  89. 250
      Packages/com.unity.transport/CHANGELOG.md
  90. 7
      Packages/com.unity.transport/CHANGELOG.md.meta
  91. 20
      Packages/com.unity.transport/DESIGN.md
  92. 7
      Packages/com.unity.transport/DESIGN.md.meta
  93. 20
      Packages/com.unity.transport/Documentation~/TableOfContents.md
  94. 17
      Packages/com.unity.transport/Documentation~/connection-state-machine.md
  95. 60
      Packages/com.unity.transport/Documentation~/event-consumption.md
  96. 94
      Packages/com.unity.transport/Documentation~/images/Pipeline-stages-diagram.png

19
Assets/Prefabs/UI/CreateContent.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: 3939988936382120653}
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: 3939988936382120653}
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: 3818918250242346738}
m_Father: {fileID: 0}

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
--- !u!1 &4576592091429739648
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: 4388255649222666380}
m_RootOrder: 0

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 18
m_fontSize: 29.35
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 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: 4903988224507325461}
m_Father: {fileID: 8663650026319491498}

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: 3818918250242346738}
m_RootOrder: 3

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: 8533460643281418118}
- {fileID: 5962604622760643426}

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: 8663650026319491498}
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: 4702713106716681505}
- {fileID: 8866965904304086500}

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: 3818918250242346738}
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: 8677466241086514681}
- {fileID: 7623034069078291411}

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: 2914824860568159748}
m_Father: {fileID: 3818918250242346738}

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: 3939988936382120653}
m_Father: {fileID: 3818918250242346738}

m_CharacterValidation: 0
m_RegexValue:
m_GlobalPointSize: 35
m_CharacterLimit: 0
m_CharacterLimit: 20
m_OnEndEdit:
m_PersistentCalls:
m_Calls: []

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: 4702713106716681505}
m_RootOrder: 0

161
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:
- {fileID: 7198558056059429223}
m_Father: {fileID: 2637199315522059511}

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: 5835473579278904550}
- {fileID: 2637199315361559713}

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: 2637199316888954227}
- {fileID: 2637199315522059511}

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: 2637199317172016811}
m_Father: {fileID: 2637199315671523625}

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: 2637199316546086228}
- {fileID: 5992334104032192704}

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: 2637199315599437355}
m_Father: {fileID: 2637199315671523625}

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: 8705625668171953304}
m_Father: {fileID: 2637199315522059511}

objectReference: {fileID: 0}
- target: {fileID: 654573747854143971, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 148.375
value: 0
value: -27.5
objectReference: {fileID: 0}
- target: {fileID: 1097905206279711496, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 3335058099760921612, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_fontSize
value: 12
value: 13.6
objectReference: {fileID: 0}
- target: {fileID: 3711939936493401148, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4532930940686182135, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_fontSize
value: 30.4
objectReference: {fileID: 0}
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 98.375
value: 0
value: -27.5
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 48.375
value: 0
value: -27.5
objectReference: {fileID: 0}
- target: {fileID: 4675500381921915110, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 5713552561910003945, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_fontSize
value: 31.3
value: 18
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 198.375
value: 0
value: -27.5
objectReference: {fileID: 0}
- target: {fileID: 6106829394853547601, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6109658478547489474, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6109658478547489474, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6109658478547489474, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6109658478547489474, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6109658478547489474, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6109658478547489474, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
value: 31.3
value: 18
objectReference: {fileID: 0}
- target: {fileID: 6701676754128905643, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8569082881819271556, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8569082881819271556, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8569082881819271556, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8569082881819271556, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8694871130870774657, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y
value: 0

- target: {fileID: 8694871130870774657, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8737752538827170250, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_fontSize
value: 18.2
objectReference: {fileID: 0}
- target: {fileID: 8941493459590097871, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

- target: {fileID: 2570977346130805250, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_fontSize
value: 17.45
objectReference: {fileID: 0}
- target: {fileID: 2603940364925728008, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2603940364925728008, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2603940364925728008, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2603940364925728008, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2603940364925728008, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2603940364925728008, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2755841879706393458, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_MinWidth
value: -1
objectReference: {fileID: 0}
- target: {fileID: 2755841879706393458, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_MinHeight
value: 100
objectReference: {fileID: 0}
- target: {fileID: 2755841879706393458, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_FlexibleWidth
value: -1
objectReference: {fileID: 0}
- target: {fileID: 2755841879706393458, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_FlexibleHeight
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2755841879706393458, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_LayoutPriority
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2755841879706393458, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_PreferredWidth
value: -1
objectReference: {fileID: 0}
- target: {fileID: 2755841879706393458, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_PreferredHeight
value: 300
objectReference: {fileID: 0}
- target: {fileID: 2828520451782533824, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_Size

893
Assets/Prefabs/UI/JoinContent.prefab
文件差异内容过多而无法显示
查看文件

91
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:
- {fileID: 1369001054989342921}
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_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 26.6
m_fontSize: 22.8
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 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: 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_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 26.6
m_fontSize: 22.8
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 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: 5919863887503833647}
- {fileID: 1119140321553661053}

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_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 1
m_PresetInfoIsWorld: 0
--- !u!114 &5836614391142406752
MonoBehaviour:
m_ObjectHideFlags: 0

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
--- !u!225 &6102798993520257211
CanvasGroup:
m_ObjectHideFlags: 0

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !u!1 &7348548600648247480
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: 5919863887503833647}
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: 6742761257817967520}
- {fileID: 2997103516229940499}

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

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

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

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

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6554840528482635367, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_fontSize
value: 35.8
objectReference: {fileID: 0}
- target: {fileID: 7315760059761538639, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchorMax.x
value: 0

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

objectReference: {fileID: 0}
- target: {fileID: 8242294458145102565, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8770739503633793913, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_fontSize
value: 21.65
objectReference: {fileID: 0}
- target: {fileID: 9032799187230319547, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_Size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 9032799187230319547, guid: c308ffc2a02e5ab4bbe70a8b2e8108c6, type: 3}
propertyPath: m_Value
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []

32
Assets/Prefabs/UI/LobbyButtonUI.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: 9095092682662508748}
m_RootOrder: 0

- component: {fileID: 1059336587790472163}
- component: {fileID: 5797179165760832527}
- component: {fileID: 5385358091761997407}
- component: {fileID: 6515571473500817606}
m_Layer: 5
m_Name: LobbyButtonUI
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: 7388874116200379571}
- {fileID: 2246652449813468979}

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
- m_Target: {fileID: 6515571473500817606}
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!114 &1059336587790472163
MonoBehaviour:
m_ObjectHideFlags: 0

m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!114 &6515571473500817606
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6423115675995281648}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4079cd003fcd20c40a3bac78acf44b55, type: 3}
m_Name:
m_EditorClassIdentifier:
m_toRecolor:
- {fileID: 4172744935978053658}
m_toggles: []
--- !u!1 &8569242987132969498
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: 9095092682662508748}
m_RootOrder: 1

14
Assets/Prefabs/UI/LobbyCodeCanvas.prefab


m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
rgba: 4292598747
m_fontColor: {r: 0.8584906, g: 0.8584906, b: 0.8584906, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_outputText: {fileID: 5578852939709204548}
m_codeType: 0
--- !u!114 &699060394989383769

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !u!1 &2798863108443093305
GameObject:
m_ObjectHideFlags: 0

m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1

m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 2150773298
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5}
rgba: 2164260863
m_fontColor: {r: 1, g: 1, b: 1, a: 0.5}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 14
m_fontSize: 20.55
m_fontSizeBase: 45
m_fontWeight: 400
m_enableAutoSizing: 1

560
Assets/Prefabs/UI/LobbyGameCanvas.prefab


- component: {fileID: 8422097929690560069}
- component: {fileID: 2804283545537723312}
m_Layer: 5
m_Name: IPText
m_Name: Relay IPText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 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_RootOrder: 0
m_RootOrder: 1
m_AnchorMin: {x: 1, y: 1}
m_AnchorMin: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 400, y: 50}
m_Pivot: {x: 1, y: 1}
m_AnchoredPosition: {x: 100, y: 0}
m_SizeDelta: {x: -100.53, y: 0}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &8422097929690560069
CanvasRenderer:
m_ObjectHideFlags: 0

m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
rgba: 4291019715
m_fontColor: {r: 0.764151, g: 0.764151, b: 0.764151, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 36
m_fontSizeBase: 36
m_fontSize: 26
m_fontSizeBase: 26
m_HorizontalAlignment: 4
m_VerticalAlignment: 1024
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0

m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_margin: {x: 5, y: 5, z: 5, w: 5}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 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
- {fileID: 466693923092094802}
- {fileID: 6520888008628790540}
- {fileID: 8966797402027724594}
- {fileID: 1906352097507614706}
- {fileID: 1529153573702653751}
- {fileID: 2957117094117392419}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !u!114 &3403950992349691351
MonoBehaviour:
m_ObjectHideFlags: 0

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
--- !u!1 &4415066352981488178
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6520888008628790540}
- component: {fileID: 2883512549879894130}
- component: {fileID: 6080528466182880233}
m_Layer: 5
m_Name: LobbyPanel
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6520888008628790540
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4415066352981488178}
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: 466693923092094802}
- {fileID: 1529153573702653751}
m_Father: {fileID: 2244251207921394025}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 400, y: -5}
m_SizeDelta: {x: 752, y: 50}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &2883512549879894130
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4415066352981488178}
m_CullTransparentMesh: 1
--- !u!114 &6080528466182880233
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4415066352981488178}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 0.392}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &4637522307789944801
GameObject:
m_ObjectHideFlags: 0

m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4637522307789944801}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_ConstrainProportionsScale: 0
m_Father: {fileID: 2244251207921394025}
m_Father: {fileID: 6520888008628790540}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 300, y: 50}
m_AnchoredPosition: {x: 1, y: 5}
m_SizeDelta: {x: 500, y: 50}
m_Pivot: {x: 0, y: 1}
--- !u!222 &2177115228008557851
CanvasRenderer:

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !u!114 &6860436446719602335
MonoBehaviour:
m_ObjectHideFlags: 0

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_lobbyNameText: {fileID: 7322885418547999459}
--- !u!225 &6749879306276389991
CanvasGroup:

m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!1 &6149947799696795246
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6532118317105062884}
- component: {fileID: 3178832907627025492}
- component: {fileID: 2080570148520651386}
m_Layer: 5
m_Name: Relay IP Label
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6532118317105062884
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6149947799696795246}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 1906352097507614706}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -300, y: 0}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &3178832907627025492
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6149947799696795246}
m_CullTransparentMesh: 1
--- !u!114 &2080570148520651386
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6149947799696795246}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: 'Relay IP:'
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 22
m_fontSizeBase: 22
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 5, z: 0, w: 5}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &6238829041546525229
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: 466693923092094802}
m_RootOrder: 0

m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: 'ServerName
m_text: 'LobbyName
'
m_isRightToLeft: 0

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 36
m_fontSize: 35.8
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_enableAutoSizing: 1
m_fontSizeMin: 20
m_fontSizeMax: 36
m_VerticalAlignment: 256
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0

m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &6874603473563550930
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8966797402027724594}
- component: {fileID: 4952411764308137619}
- component: {fileID: 5059550841224826565}
m_Layer: 5
m_Name: RelayPanel
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8966797402027724594
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6874603473563550930}
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: 1906352097507614706}
- {fileID: 2957117094117392419}
m_Father: {fileID: 2244251207921394025}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 400, y: -255}
m_SizeDelta: {x: 750, y: 55}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &4952411764308137619
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6874603473563550930}
m_CullTransparentMesh: 1
--- !u!114 &5059550841224826565
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6874603473563550930}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 0.392}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &7079534792968695919
GameObject:
m_ObjectHideFlags: 0

m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7079534792968695919}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_ConstrainProportionsScale: 0
- {fileID: 6532118317105062884}
m_Father: {fileID: 2244251207921394025}
m_RootOrder: 3
m_Father: {fileID: 8966797402027724594}
m_RootOrder: 0
m_AnchorMin: {x: 1, y: 1}
m_AnchorMin: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 400, y: 50}
m_Pivot: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -5}
m_SizeDelta: {x: -350, y: -10}
m_Pivot: {x: 0, y: 1}
--- !u!222 &8834330287924717091
CanvasRenderer:
m_ObjectHideFlags: 0

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !u!114 &279783410280127446
MonoBehaviour:
m_ObjectHideFlags: 0

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_IPAddressText: {fileID: 2804283545537723312}
--- !u!225 &8630015524497407890
CanvasGroup:

m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
m_TransformParent: {fileID: 2244251207921394025}
m_TransformParent: {fileID: 8966797402027724594}
value: 20.55
value: 25.95
value: 0
value: 1
value: 0
value: 1
value: 5
value: 1
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4102997489641105917, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 4102997489641105917, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_AnchorMin.y
value: 1
value: 0
value: 200
objectReference: {fileID: 0}
- target: {fileID: 4102997489641105917, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_SizeDelta.y
value: 35
value: -550
objectReference: {fileID: 0}
- target: {fileID: 4102997489641105917, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_LocalPosition.x

objectReference: {fileID: 0}
- target: {fileID: 4102997489641105917, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_AnchoredPosition.x
value: 24
value: 0
value: -323.4
value: -5
objectReference: {fileID: 0}
- target: {fileID: 4102997489641105917, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_LocalEulerAnglesHint.x

propertyPath: m_Name
value: RelayCodeCanvas
objectReference: {fileID: 0}
- target: {fileID: 7546827419935918100, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_lineSpacing
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7546827419935918100, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_fontSizeBase
value: 18
objectReference: {fileID: 0}
- target: {fileID: 7546827419935918100, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_enableAutoSizing
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7546827419935918100, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_enableWordWrapping
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7676491730539518990, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_Alpha
value: 0

objectReference: {fileID: 0}
- target: {fileID: 1919168897190896396, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 167.125
value: 0
value: -27.5
objectReference: {fileID: 0}
- target: {fileID: 2056817220376623591, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 2507055715224942121, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_RootOrder
value: 2
value: 3
objectReference: {fileID: 0}
- target: {fileID: 2507055715224942121, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.x

propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2987822160017223264, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_toRecolor.Array.size
value: 6
objectReference: {fileID: 0}
- target: {fileID: 2987822160017223264, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_toRecolor.Array.data[3]
value:
objectReference: {fileID: 7322885418547999459}
- target: {fileID: 2987822160017223264, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_toRecolor.Array.data[4]
value:
objectReference: {fileID: 2768469221279893250}
- target: {fileID: 2987822160017223264, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_toRecolor.Array.data[5]
value:
objectReference: {fileID: 3223430358889797224}
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 67.125
value: 0
value: -27.5
value: 0
value: 1
value: 0
value: 1
value: 0
value: 50
value: 0
value: 50
value: 0
value: 117.125
value: 0
value: -27.5
objectReference: {fileID: 0}
- target: {fileID: 4467363028704636643, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_fontSize
value: 14.9
objectReference: {fileID: 0}
- target: {fileID: 4558362294547660329, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMin.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_SizeDelta.x
value: 50
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_SizeDelta.y
value: 50
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.x
value: 217.125
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.y
value: -27.5
objectReference: {fileID: 0}
- target: {fileID: 6687484736792569641, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_Enabled
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7303921398628037483, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7303921398628037483, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7303921398628037483, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7303921398628037483, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8979361099148208042, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y
value: 0

m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
m_TransformParent: {fileID: 2244251207921394025}
m_TransformParent: {fileID: 6520888008628790540}
m_Modifications:
- target: {fileID: 1118541987231860824, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
propertyPath: m_Name

propertyPath: m_Pivot.x
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1118541987231860827, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
propertyPath: m_Pivot.y

propertyPath: m_RootOrder
value: 4
value: 1
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1118541987231860827, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchorMin.x
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1118541987231860827, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
propertyPath: m_AnchorMin.y

objectReference: {fileID: 0}
- target: {fileID: 1118541987231860827, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
propertyPath: m_AnchoredPosition.x
value: 24
value: -1
value: -288.4
value: -42.9
objectReference: {fileID: 0}
- target: {fileID: 1118541987231860827, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
propertyPath: m_LocalEulerAnglesHint.x

m_RemovedComponents:
- {fileID: 6429120237646330959, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
m_SourcePrefab: {fileID: 100100000, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
--- !u!114 &2768469221279893250 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 4383951900022528110, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
m_PrefabInstance: {fileID: 1926829325626138476}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &3223430358889797224 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 3892963601838131460, guid: 81e22025e6659264a8d0bc8dfebe95a7, type: 3}
m_PrefabInstance: {fileID: 1926829325626138476}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
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}

objectReference: {fileID: 0}
- target: {fileID: 4463750083940306578, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_Enabled
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4463750083940306578, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_PresetInfoIsWorld
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4463750083940306589, guid: e269788e17cbca145bf78e8971aeb223, type: 3}

130
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_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
m_PresetInfoIsWorld: 1
--- !u!114 &4463750083940306577
MonoBehaviour:
m_ObjectHideFlags: 0

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_UserUIObjects:
- {fileID: 8208727230702623701}
- {fileID: 4503217775887893542}

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !u!1 &5373112041518163852
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: 4463750083940306590}
m_RootOrder: 1

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_ShowThisWhen: 4
--- !u!114 &7056422356970708528
MonoBehaviour:

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 0
--- !u!1 &5447062805889817511
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: 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}

propertyPath: m_Name
value: UserUI_4
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2889154384621986752, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 3540056819003737500, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_fontSize
value: 58
value: 26.15
value: 72
value: 26.15
objectReference: {fileID: 0}
- target: {fileID: 5235189765028238254, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: showing

propertyPath: m_Name
value: UserUI_1
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2889154384621986752, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 3540056819003737500, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_fontSize
value: 58
value: 26.15
value: 72
value: 26.15
objectReference: {fileID: 0}
- target: {fileID: 5804120253616419419, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y

- target: {fileID: 5804120253616419419, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6496917371405060994, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7147564553318460541, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y

- target: {fileID: 1767503274657767312, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_Name
value: UserUI_3
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2889154384621986752, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 3540056819003737500, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_fontSize
value: 58
value: 26.15
value: 72
value: 26.15
objectReference: {fileID: 0}
- target: {fileID: 5235189765028238254, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: showing

propertyPath: m_Name
value: UserUI_2
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2599817509811505645, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2889154384621986752, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 3540056819003737500, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: m_fontSize
value: 58
value: 26.15
value: 72
value: 26.15
objectReference: {fileID: 0}
- target: {fileID: 5235189765028238254, guid: 9c09208dc6d58f54aabb57c12071b660, type: 3}
propertyPath: showing

868
Assets/Prefabs/UI/PlayerInteractionPanel.prefab
文件差异内容过多而无法显示
查看文件

30
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_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 2150773298
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5}
rgba: 2164260863
m_fontColor: {r: 1, g: 1, b: 1, a: 0.5}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 72
m_fontSize: 20.55
m_fontSizeBase: 45
m_fontWeight: 400
m_enableAutoSizing: 1

m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 3, y: 3, z: 3, w: 3}
m_margin: {x: 3, y: 5, z: 3, w: 5}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 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_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 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: 4102997489641105917}
m_RootOrder: 0

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 68.3
m_fontSize: 18
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1

m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_margin: {x: 0, y: 5, z: 0, w: 5}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 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: 5389159257502379616}
- {fileID: 6476122475963766563}

m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -247.72728, y: 234.57413}
m_SizeDelta: {x: -495.4545, y: -319.1482}
m_SizeDelta: {x: 200, y: -10}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4102997489641105913
CanvasRenderer:

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 1
m_outputText: {fileID: 8798075752901962210}
m_codeType: 1
--- !u!114 &4523467532116611583

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !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

m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
rgba: 4293388263
m_fontColor: {r: 0.9056604, g: 0.9056604, b: 0.9056604, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:

m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 3, y: 3, z: 3, w: 3}
m_margin: {x: 3, y: 5, z: 3, w: 5}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0

19
Assets/Prefabs/UI/RenamePopup.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: 4299827590990313863}
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: 754765198059719065}
- {fileID: 4300767933049215827}

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: 4582680168689072900}
m_Father: {fileID: 1113109783147550039}

m_CharacterValidation: 0
m_RegexValue:
m_GlobalPointSize: 30
m_CharacterLimit: 0
m_CharacterLimit: 24
m_OnEndEdit:
m_PersistentCalls:
m_Calls:

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: 1205332528783832187}
- {fileID: 4299827590990313863}

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: 4582680168689072900}
m_RootOrder: 0

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 28.6
m_fontSize: 24.15
m_fontSizeBase: 14
m_fontWeight: 400
m_enableAutoSizing: 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: 4582680168689072900}
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: 8624457953407905706}
m_Father: {fileID: 0}

m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 1
m_PresetInfoIsWorld: 0
--- !u!225 &7663265756039998349
CanvasGroup:
m_ObjectHideFlags: 0

m_Script: {fileID: 11500000, guid: 160dfc78f3641c94fbebe419be087996, type: 3}
m_Name:
m_EditorClassIdentifier:
showing: 0
m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
--- !u!1 &8624457953407905705
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: 1113109783147550039}
m_Father: {fileID: 8624457953394683849}

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: 557953118367534400}
m_Father: {fileID: 1113109783147550039}

174
Assets/Prefabs/UI/UserCardPanel.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_RootOrder: 0
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}

m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 100
m_PreferredWidth: 600
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -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: 5804120253616419419}
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_RootOrder: 2
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}

m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredWidth: 89.99
m_LayoutPriority: 1
m_LayoutPriority: 3
--- !u!1 &1767503274657767312
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
- {fileID: 2599817509811505645}
- {fileID: 5804120253616419419}
- {fileID: 2889154384621986752}
- {fileID: 7147564553318460541}

m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_HostIcon: {fileID: 2063326445664711249}
--- !u!114 &788426075660952210
MonoBehaviour:
m_ObjectHideFlags: 0

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 0
--- !u!114 &7885056472121154813
MonoBehaviour:
m_ObjectHideFlags: 0

m_Right: 3
m_Top: 3
m_Bottom: 3
m_ChildAlignment: 1
m_ChildAlignment: 0
m_ChildForceExpandWidth: 1
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 1
m_ChildControlWidth: 1
m_ChildControlHeight: 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: 5804120253616419419}
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: []
m_Father: {fileID: 5804120253616419419}
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_RootOrder: 1
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}

m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredWidth: 200
m_LayoutPriority: 1
m_LayoutPriority: 2
--- !u!1 &6163925503969852289
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7160561887711141280}
- component: {fileID: 4467506543349070405}
- component: {fileID: 2063326445664711249}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7160561887711141280
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6163925503969852289}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 2599817509811505645}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: -10}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4467506543349070405
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6163925503969852289}
m_CullTransparentMesh: 1
--- !u!114 &2063326445664711249
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6163925503969852289}
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 57c53a150c76b1f4c9fe27ee2b9a1d17, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &6191246080836473555
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: 7147564553318460541}
m_RootOrder: 0

m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &6496917371405060994
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2599817509811505645}
- component: {fileID: 2767003350947099766}
- component: {fileID: 3258164575509591423}
m_Layer: 5
m_Name: HostIconArea
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2599817509811505645
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6496917371405060994}
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: 7160561887711141280}
m_Father: {fileID: 3229036008637484624}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 1, y: 0.5}
--- !u!222 &2767003350947099766
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6496917371405060994}
m_CullTransparentMesh: 1
--- !u!114 &3258164575509591423
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6496917371405060994}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 40
m_MinHeight: 40
m_PreferredWidth: 40
m_PreferredHeight: 40
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 0
--- !u!1 &9038849392571924126
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: 2889154384621986752}
m_RootOrder: 0

2
Assets/Prefabs/GameManager.prefab


- component: {fileID: 7716713811812636910}
- component: {fileID: 5193415626965589893}
m_Layer: 0
m_Name: GameStateManager
m_Name: GameManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0

767
Assets/Scenes/mainScene.unity
文件差异内容过多而无法显示
查看文件

12
Assets/Scripts/Auth/Identity.cs


namespace LobbyRelaySample.Auth
{
/// <summary>
/// Each context will have its own identity needs, so we'll allow each to define whatever parameters it needs.
/// Represents some provider of credentials.
/// Each provider will have its own identity needs, so we'll allow each to define whatever parameters it needs.
/// Anything that accesses the contents should know what it's looking for.
/// </summary>
public class SubIdentity : Observed<SubIdentity>

}
/// <summary>
/// Our internal representation of a player, wrapping the data required for interfacing with the identities of that player in the services.
/// One will be created for the local player, as well as for each other member of the lobby.
/// Our internal representation of the local player's credentials, wrapping the data required for interfacing with the identities of that player in the services.
/// (In use here, it just wraps Auth, but it can be used to combine multiple sets of credentials into one concept of a player.)
public Identity()
{
m_subIdentities.Add(IIdentityType.Local, new SubIdentity());
m_subIdentities.Add(IIdentityType.Auth, new SubIdentity_Authentication());
}
public Identity(Action callbackOnAuthLogin)
{
m_subIdentities.Add(IIdentityType.Local, new SubIdentity());

6
Assets/Scripts/Auth/NameGenerator.cs


namespace LobbyRelaySample
{
/// <summary>
/// Just for fun, give a cute default player name if no name is provided.
/// Just for fun, give a cute default player name if no name is provided, based on a hash of their anonymous ID.
/// </summary>
public static class NameGenerator
{

else if (word == 1)
nameOutput.Append("Bear");
else if (word == 2)
nameOutput.Append("Cow");
nameOutput.Append("Crow");
else if (word == 3)
nameOutput.Append("Dog");
else if (word == 4)

else if (word == 15)
nameOutput.Append("Puffin");
else if (word == 16)
nameOutput.Append("Raven");
nameOutput.Append("Rabbit");
else if (word == 17)
nameOutput.Append("Snake");
else if (word == 18)

4
Assets/Scripts/Auth/SubIdentity_Authentication.cs


private async void DoSignIn(Action onSigninComplete)
{
await UnityServices.Initialize();
await UnityServices.InitializeAsync();
await AuthenticationService.Instance.SignInAnonymouslyAsync(); // Note: We don't want to sign out later, since that changes the UAS anonymous token, which would prevent the player from exiting lobbies they're already in.
await AuthenticationService.Instance.SignInAnonymouslyAsync(); // Don't sign out later, since that changes the anonymous token, which would prevent the player from exiting lobbies they're already in.
onSigninComplete?.Invoke();
}

48
Assets/Scripts/Infrastructure/Locator.cs


namespace LobbyRelaySample
{
/// <summary>
/// Anything which provides itself to a Locator can then be globally accessed. This should be a single access point for things that *want* to be singleton (that is,
/// when they want to be available for use by arbitrary, unknown clients) but might not always be available or might need alternate flavors for tests, logging, etc.
/// </summary>
public class Locator : LocatorBase
{
private static Locator s_instance;
public static Locator Get
{
get
{
if (s_instance == null)
s_instance = new Locator();
return s_instance;
}
}
protected override void FinishConstruction()
{
s_instance = this;
}
}
/// <summary>
/// Allows Located services to transfer data to their replacements if needed.
/// </summary>
/// <typeparam name="T">The base interface type you want to Provide.</typeparam>

public void Provide(IIdentity identity) { ProvideAny(identity); }
// As you add more Provided types, be sure their default implementations are included in the constructor.
}
/// <summary>
/// Anything which provides itself to a Locator can then be globally accessed. This should be a single access point for things that *want* to be singleton (that is,
/// when they want to be available for use by arbitrary, unknown clients) but might not always be available or might need alternate flavors for tests, logging, etc.
/// </summary>
public class Locator : LocatorBase
{
private static Locator s_instance;
public static Locator Get
{
get
{
if (s_instance == null)
s_instance = new Locator();
return s_instance;
}
}
protected override void FinishConstruction()
{
s_instance = this;
}
}
}

48
Assets/Scripts/Infrastructure/LogHandler.cs


using System;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace LobbyRelaySample

Critical, // Errors only.
Critical, // Errors only
Warnings, // Errors and Warnings
Verbose // Everything
}

public LogMode mode = LogMode.Critical;
static LogHandler s_instance;
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; //Store the unity default logger to print to console.
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; // Store the default logger that prints to console.
ErrorReaction m_reaction;
public static LogHandler Get()
{

return s_instance;
}
public void SetLogReactions(ErrorReaction reactions)
{
m_reaction = reactions;
}
if (logType == LogType.Exception) // Exceptions are captured by LogException?
if (logType == LogType.Exception) // Exceptions are captured by LogException and should always be logged.
return;
if (logType == LogType.Error || logType == LogType.Assert)

public void LogException(Exception exception, Object context)
{
LogReaction(exception);
}
private void LogReaction(Exception exception)
{
m_reaction?.Filter(exception);
}
}
/// <summary>
/// The idea here is to present the most relevant error first.
/// </summary>
[Serializable]
public class ErrorReaction
{
public UnityEvent<string> m_logMessageCallback;
public void Filter(Exception exception)
{
string message = "";
var rawExceptionMessage = "";
// We want to ensure the most relevant error message is on top.
if (exception.InnerException != null)
rawExceptionMessage = exception.InnerException.ToString();
else
rawExceptionMessage = exception.ToString();
var firstLineIndex = rawExceptionMessage.IndexOf("\n");
var firstRelayString = rawExceptionMessage.Substring(0, firstLineIndex);
message = firstRelayString;
if (string.IsNullOrEmpty(message))
return;
m_logMessageCallback?.Invoke(message);
}
}
}

84
Assets/Scripts/Infrastructure/Messenger.cs


namespace LobbyRelaySample
{
/// <summary>
/// Ensure that message contents are obvious but not dependent on spelling strings correctly.
/// </summary>
public enum MessageType
{
// These are assigned arbitrary explicit values so that if a MessageType is serialized and more enum values are later inserted/removed, the serialized values need not be reassigned.
// (If you want to remove a message, make sure it isn't serialized somewhere first.)
None = 0,
RenameRequest = 1,
JoinLobbyRequest = 2,
CreateLobbyRequest = 3,
QueryLobbies = 4,
PlayerJoinedLobby = 5,
PlayerLeftLobby = 6,
ChangeGameState = 7,
ChangeLobbyUserState = 8,
HostInitReadyCheck = 9,
LocalUserReadyCheckResponse = 10,
UserSetEmote = 11,
ToLobby = 12,
Client_EndReadyCountdownAt = 13,
}
/// <summary>
/// Something that wants to subscribe to messages from arbitrary, unknown senders.
/// </summary>
public interface IReceiveMessages
{
void OnReceiveMessage(MessageType type, object msg);
}
/// <summary>
/// Something to which IReceiveMessages can send/subscribe for arbitrary messages.
/// </summary>
public interface IMessenger : IReceiveMessages, IProvidable<IMessenger>
{
void Subscribe(IReceiveMessages receiver);
void Unsubscribe(IReceiveMessages receiver);
}
/// <summary>
/// This allows components with unrelated responsibilities to interact without becoming coupled, since message senders don't
/// need to know what (if anything) is receiving their messages.
/// </summary>
public class Messenger : IMessenger
{

m_receivers.Remove(receiver);
}
/// <summary>
/// Send a message to any subscribers, who will decide how to handle the message.
/// </summary>
/// <param name="msg">If there's some data relevant to the recipient, include it here.</param>
public virtual void OnReceiveMessage(MessageType type, object msg)
{
Stopwatch stopwatch = new Stopwatch();

if (previousProvider is Messenger)
m_receivers.AddRange((previousProvider as Messenger).m_receivers);
}
}
/// <summary>
/// Ensure that message contents are obvious but not dependent on spelling strings correctly.
/// </summary>
public enum MessageType
{
// These are assigned arbitrary explicit values so that if a MessageType is serialized and more enum values are later inserted/removed, the serialized values need not be reassigned.
// (If you want to remove a message, make sure it isn't serialized somewhere first.)
None = 0,
RenameRequest = 1,
JoinLobbyRequest = 2,
CreateLobbyRequest = 3,
QueryLobbies = 4,
ChangeGameState = 5,
LobbyUserStatus = 6,
UserSetEmote = 7,
EndGame = 8,
StartCountdown = 9,
CancelCountdown = 10,
ConfirmInGameState = 11,
}
/// <summary>
/// Something that wants to subscribe to messages from arbitrary, unknown senders.
/// </summary>
public interface IReceiveMessages
{
void OnReceiveMessage(MessageType type, object msg);
}
/// <summary>
/// Something to which IReceiveMessages can send/subscribe for arbitrary messages.
/// </summary>
public interface IMessenger : IReceiveMessages, IProvidable<IMessenger>
{
void Subscribe(IReceiveMessages receiver);
void Unsubscribe(IReceiveMessages receiver);
}
}

7
Assets/Scripts/Infrastructure/Observed.cs


namespace LobbyRelaySample
{
/// <summary>
/// In your Observed children, be sure to call OnChanged when setting the value of any property.
/// 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.
///
/// In your Observed child implementations, be sure to call OnChanged when setting the value of any property.
/// <typeparam name="T">The Data we want to view.</typeparam>
/// <typeparam name="T">The type of object to be observed.</typeparam>
public abstract class Observed<T>
{
/// <summary>

9
Assets/Scripts/Infrastructure/ObserverBehaviour.cs


namespace LobbyRelaySample
{
/// <summary>
/// Observes an Observed class, intitializes with Observed State when beginning observation
/// MonoBehaviour that will automatically handle setting up to observe something. It also exposes an event so some other component can effectively observe it as well.
/// <typeparam name="T"></typeparam>
/// <summary>
/// Option to allow certain observers to not be registered by the GameStateManager automatically.
/// </summary>
public bool observeOnStart = true;
protected virtual void UpdateObserver(T obs)
{

163
Assets/Scripts/Infrastructure/UpdateSlow.cs


{
public delegate void UpdateMethod(float dt);
public interface IUpdateSlow : IProvidable<IUpdateSlow>
{
void OnUpdate(float dt);
void Subscribe(UpdateMethod onUpdate);
void Unsubscribe(UpdateMethod onUpdate);
}
/// A default implementation.
/// </summary>
public class UpdateSlowNoop : IUpdateSlow
{
public void OnUpdate(float dt) { }
public void Subscribe(UpdateMethod onUpdate) { }
public void Unsubscribe(UpdateMethod onUpdate) { }
public void OnReProvided(IUpdateSlow prev) { }
}
/// <summary>
/// Some objects might need to be on a slower update loop than the usual MonoBehaviour Update, e.g. to refresh data from services.
/// Some objects might need to be on a slower update loop than the usual MonoBehaviour Update and without precise timing, e.g. to refresh data from services.
[SerializeField]
[Tooltip("Update interval. Note that lobby Get requests must occur at least 1 second apart, so this period should likely be greater than that.")]
private float m_updatePeriod = 1.5f;
private class Subscriber
{
public UpdateMethod updateMethod;
public readonly float period;
public float periodCurrent;
public Subscriber(UpdateMethod updateMethod, float period)
{
this.updateMethod = updateMethod;
this.period = period;
this.periodCurrent = 0;
}
}
[SerializeField]
[Tooltip("If a subscriber to slow update takes longer than this to execute, it can be automatically unsubscribed.")]
private float m_durationToleranceMs = 10;

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

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

m_subscribers.AddRange((prevUpdateSlow as UpdateSlow).m_subscribers);
}
}
public interface IUpdateSlow : IProvidable<IUpdateSlow>
{
void OnUpdate(float dt);
void Subscribe(UpdateMethod onUpdate, float period);
void Unsubscribe(UpdateMethod onUpdate);
}
/// <summary>
/// A default implementation.
/// </summary>
public class UpdateSlowNoop : IUpdateSlow
{
public void OnUpdate(float dt) { }
public void Subscribe(UpdateMethod onUpdate, float period) { }
public void Unsubscribe(UpdateMethod onUpdate) { }
public void OnReProvided(IUpdateSlow prev) { }
}
}

118
Assets/Scripts/Lobby/LobbyAPIInterface.cs


using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;

/// Does all the interactions with the Lobby API.
/// Wrapper for all the interactions with the Lobby API.
private class InProgressRequest<T>
private const int k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
public static void CreateLobbyAsync(string requesterUASId, string lobbyName, int maxPlayers, bool isPrivate, Dictionary<string, PlayerDataObject> localUserData, Action<Lobby> onComplete)
public InProgressRequest(Task<T> task, Action<T> onComplete)
CreateLobbyOptions createOptions = new CreateLobbyOptions
DoRequest(task, onComplete);
}
private async void DoRequest(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace;
try {
result = await task;
} catch (Exception e) {
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
} finally {
onComplete?.Invoke(result);
}
}
IsPrivate = isPrivate,
Player = new Player(id: requesterUASId, data: localUserData)
};
var task = Lobbies.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
AsyncRequest.DoRequest(task, onComplete);
private const int k_maxLobbiesToShow = 64;
public static void CreateLobbyAsync(string requesterUASId, string lobbyName, int maxPlayers, bool isPrivate, Action<Response<Lobby>> onComplete)
public static void DeleteLobbyAsync(string lobbyId, Action onComplete)
CreateLobbyRequest createRequest = new CreateLobbyRequest(new CreateRequest(
name: lobbyName,
player: new Player(requesterUASId),
maxPlayers: maxPlayers,
isPrivate: isPrivate
));
var task = LobbyService.LobbyApiClient.CreateLobbyAsync(createRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
var task = Lobbies.Instance.DeleteLobbyAsync(lobbyId);
AsyncRequest.DoRequest(task, onComplete);
public static void DeleteLobbyAsync(string lobbyId, Action<Response> onComplete)
public static void JoinLobbyAsync_ByCode(string requesterUASId, string lobbyCode, Dictionary<string, PlayerDataObject> localUserData, Action<Lobby> onComplete)
DeleteLobbyRequest deleteRequest = new DeleteLobbyRequest(lobbyId);
var task = LobbyService.LobbyApiClient.DeleteLobbyAsync(deleteRequest);
new InProgressRequest<Response>(task, onComplete);
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions { Player = new Player(id: requesterUASId, data: localUserData) };
var task = Lobbies.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void JoinLobbyAsync_ByCode(string requesterUASId, string lobbyCode, Action<Response<Lobby>> onComplete)
public static void JoinLobbyAsync_ById(string requesterUASId, string lobbyId, Dictionary<string, PlayerDataObject> localUserData, Action<Lobby> onComplete)
JoinLobbyByCodeRequest joinRequest = new JoinLobbyByCodeRequest(new JoinByCodeRequest(lobbyCode, new Player(requesterUASId)));
var task = LobbyService.LobbyApiClient.JoinLobbyByCodeAsync(joinRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions { Player = new Player(id: requesterUASId, data: localUserData) };
var task = Lobbies.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void JoinLobbyAsync_ById(string requesterUASId, string lobbyId, Action<Response<Lobby>> onComplete)
public static void LeaveLobbyAsync(string requesterUASId, string lobbyId, Action onComplete)
JoinLobbyByIdRequest joinRequest = new JoinLobbyByIdRequest(lobbyId, new Player(requesterUASId));
var task = LobbyService.LobbyApiClient.JoinLobbyByIdAsync(joinRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
var task = Lobbies.Instance.RemovePlayerAsync(lobbyId, requesterUASId);
AsyncRequest.DoRequest(task, onComplete);
public static void LeaveLobbyAsync(string requesterUASId, string lobbyId, Action<Response> onComplete)
public static void QueryAllLobbiesAsync(List<QueryFilter> filters, Action<QueryResponse> onComplete)
RemovePlayerRequest leaveRequest = new RemovePlayerRequest(lobbyId, requesterUASId);
var task = LobbyService.LobbyApiClient.RemovePlayerAsync(leaveRequest);
new InProgressRequest<Response>(task, onComplete);
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
var task = Lobbies.Instance.QueryLobbiesAsync(queryOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void QueryAllLobbiesAsync(Action<Response<QueryResponse>> onComplete)
public static void GetLobbyAsync(string lobbyId, Action<Lobby> onComplete)
QueryLobbiesRequest queryRequest = new QueryLobbiesRequest(new QueryRequest(count: k_maxLobbiesToShow));
var task = LobbyService.LobbyApiClient.QueryLobbiesAsync(queryRequest);
new InProgressRequest<Response<QueryResponse>>(task, onComplete);
var task = Lobbies.Instance.GetLobbyAsync(lobbyId);
AsyncRequest.DoRequest(task, onComplete);
public static void GetLobbyAsync(string lobbyId, Action<Response<Lobby>> onComplete)
public static void UpdateLobbyAsync(string lobbyId, Dictionary<string, DataObject> data, Action<Lobby> onComplete)
GetLobbyRequest getRequest = new GetLobbyRequest(lobbyId);
var task = LobbyService.LobbyApiClient.GetLobbyAsync(getRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = data };
var task = Lobbies.Instance.UpdateLobbyAsync(lobbyId, updateOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void UpdateLobbyAsync(string lobbyId, Dictionary<string, DataObject> data, Action<Response<Lobby>> onComplete)
public static void UpdatePlayerAsync(string lobbyId, string playerId, Dictionary<string, PlayerDataObject> data, Action<Lobby> onComplete, string allocationId, string connectionInfo)
UpdateLobbyRequest updateRequest = new UpdateLobbyRequest(lobbyId, new UpdateRequest(
data: data
));
var task = LobbyService.LobbyApiClient.UpdateLobbyAsync(updateRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = data,
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
var task = Lobbies.Instance.UpdatePlayerAsync(lobbyId, playerId, updateOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void UpdatePlayerAsync(string lobbyId, string playerId, Dictionary<string, PlayerDataObject> data, Action<Response<Lobby>> onComplete)
public static void HeartbeatPlayerAsync(string lobbyId)
UpdatePlayerRequest updateRequest = new UpdatePlayerRequest(lobbyId, playerId, new PlayerUpdateRequest(
data: data
));
var task = LobbyService.LobbyApiClient.UpdatePlayerAsync(updateRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
var task = Lobbies.Instance.SendHeartbeatPingAsync(lobbyId);
AsyncRequest.DoRequest(task, null);
}
}
}

248
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


using System;
using System.Collections.Generic;
using Unity.Services.Authentication;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
namespace LobbyRelaySample

public LobbyAsyncRequests()
{
Locator.Get.UpdateSlow.Subscribe(UpdateLobby); // Shouldn't need to unsubscribe since this instance won't be replaced.
}
private static bool IsSuccessful(Response response)
{
return response != null && response.Status >= 200 && response.Status < 300; // Uses HTTP status codes, so 2xx is a success.
Locator.Get.UpdateSlow.Subscribe(UpdateLobby, 0.5f); // Shouldn't need to unsubscribe since this instance won't be replaced. 0.5s is arbitrary; the rate limits are tracked later.
#region We want to cache the lobby object so we don't query for it every time we need to do a different lobby operation or view current data.
#region Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
private Queue<Action> m_pendingOperations = new Queue<Action>();
private bool m_isMidRetrieve = false;
public Lobby CurrentLobby => m_lastKnownLobby;
public void BeginTracking(string lobbyId)

public void EndTracking()
{
m_currentLobbyId = null;
m_lastKnownLobby = null;
m_heartbeatTime = 0;
}
private void UpdateLobby(float unused)

void OnComplete(Lobby lobby)
{
if (lobby != null)
{
m_isMidRetrieve = false;
HandlePendingOperations();
}
private void HandlePendingOperations()
#endregion
#region Lobby API calls are rate limited, and some other operations might want an alert when the rate limits have passed.
// Note that some APIs limit to 1 call per N seconds, while others limit to M calls per N seconds. We'll treat all APIs as though they limited to 1 call per N seconds.
public enum RequestType { Query = 0, Join }
public RateLimitCooldown GetRateLimit(RequestType type)
while (m_pendingOperations.Count > 0)
m_pendingOperations.Dequeue()?.Invoke(); // Note: If this ends up enqueuing a bunch of operations, we might need to batch them and/or ensure they don't all execute at once.
if (type == RequestType.Join)
return m_rateLimitJoin;
return m_rateLimitQuery;
private RateLimitCooldown m_rateLimitQuery = new RateLimitCooldown(1.5f); // Used for both the lobby list UI and the in-lobby updating. In the latter case, updates can be cached.
private RateLimitCooldown m_rateLimitJoin = new RateLimitCooldown(3f);
// TODO: Shift to using this to do rate limiting for all API calls? E.g. the lobby data pushing is on its own loop.
private static Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser player)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
PlayerDataObject dataObjName = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, player.DisplayName);
data.Add("DisplayName", dataObjName);
return data;
}
public void CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, Action<Lobby> onSuccess, Action onFailure)
public void CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LobbyUser localUser, Action<Lobby> onSuccess, Action onFailure)
LobbyAPIInterface.CreateLobbyAsync(uasId, lobbyName, maxPlayers, isPrivate, OnLobbyCreated);
LobbyAPIInterface.CreateLobbyAsync(uasId, lobbyName, maxPlayers, isPrivate, CreateInitialPlayerData(localUser), OnLobbyCreated);
void OnLobbyCreated(Response<Lobby> response)
void OnLobbyCreated(Lobby response)
if (!IsSuccessful(response))
if (response == null)
{
var pendingLobby = response.Result;
onSuccess?.Invoke(pendingLobby); // The Create request automatically joins the lobby, so we need not take further action.
}
onSuccess?.Invoke(response); // The Create request automatically joins the lobby, so we need not take further action.
/// <summary>Attempt to join an existing lobby. Either ID xor code can be null.</summary>
public void JoinLobbyAsync(string lobbyId, string lobbyCode, Action<Lobby> onSuccess, Action onFailure)
/// <summary>
/// Attempt to join an existing lobby. Either ID xor code can be null.
/// </summary>
public void JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser, Action<Lobby> onSuccess, Action onFailure)
if (!m_rateLimitJoin.CanCall() ||
(lobbyId == null && lobbyCode == null))
{
onFailure?.Invoke();
// TODO: Emit some failure message.
return;
}
LobbyAPIInterface.JoinLobbyAsync_ById(uasId, lobbyId, OnLobbyJoined);
LobbyAPIInterface.JoinLobbyAsync_ById(uasId, lobbyId, CreateInitialPlayerData(localUser), OnLobbyJoined);
LobbyAPIInterface.JoinLobbyAsync_ByCode(uasId, lobbyCode, OnLobbyJoined);
LobbyAPIInterface.JoinLobbyAsync_ByCode(uasId, lobbyCode, CreateInitialPlayerData(localUser), OnLobbyJoined);
void OnLobbyJoined(Response<Lobby> response)
void OnLobbyJoined(Lobby response)
if (!IsSuccessful(response))
if (response == null)
onSuccess?.Invoke(response?.Result);
onSuccess?.Invoke(response);
/// <summary>Used for getting the list of all active lobbies, without needing full info for each.</summary>
/// <summary>
/// Used for getting the list of all active lobbies, without needing full info for each.
/// </summary>
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<Response<QueryResponse>> onError = null)
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<QueryResponse> onError = null, LobbyColor limitToColor = LobbyColor.None)
LobbyAPIInterface.QueryAllLobbiesAsync(OnLobbyListRetrieved);
if (!m_rateLimitQuery.CanCall())
{
onListRetrieved?.Invoke(null);
m_rateLimitQuery.EnqueuePendingOperation(() => { RetrieveLobbyListAsync(onListRetrieved, onError, limitToColor); });
return;
}
void OnLobbyListRetrieved(Response<QueryResponse> response)
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));
LobbyAPIInterface.QueryAllLobbiesAsync(filters, OnLobbyListRetrieved);
void OnLobbyListRetrieved(QueryResponse response)
if (IsSuccessful(response))
onListRetrieved?.Invoke(response?.Result);
if (response != null)
onListRetrieved?.Invoke(response);
/// <param name="onComplete">If no lobby is retrieved, this is given null.</param>
/// <param name="onComplete">If no lobby is retrieved, or if this call hits the rate limit, this is given null.</param>
if (m_isMidRetrieve)
return; // Not calling onComplete since there's just the one point at which this is called.
m_isMidRetrieve = true;
if (!m_rateLimitQuery.CanCall())
{
onComplete?.Invoke(null);
return;
}
void OnGet(Response<Lobby> response)
void OnGet(Lobby response)
m_isMidRetrieve = false;
onComplete?.Invoke(response?.Result);
onComplete?.Invoke(response);
}
}

string uasId = AuthenticationService.Instance.PlayerId;
LobbyAPIInterface.LeaveLobbyAsync(uasId, lobbyId, OnLeftLobby);
void OnLeftLobby(Response response)
void OnLeftLobby()
// TEMP. As of 6/31/21, the lobbies service doesn't automatically delete emptied lobbies, though that functionality is expected in the near-term.
// Until then, we'll do a delete request whenever we leave, and if it's invalid, we'll just get a 403 back.
LobbyAPIInterface.DeleteLobbyAsync(lobbyId, null);
}
}

if (!ShouldUpdateData(() => { UpdatePlayerDataAsync(data, onComplete); }, onComplete))
if (!ShouldUpdateData(() => { UpdatePlayerDataAsync(data, onComplete); }, onComplete, false))
Lobby lobby = m_lastKnownLobby;
string playerId = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{

dataCurr.Add(dataNew.Key, dataObj);
}
LobbyAPIInterface.UpdatePlayerAsync(lobby.Id, Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id"), dataCurr, (r) => { onComplete?.Invoke(); });
LobbyAPIInterface.UpdatePlayerAsync(m_lastKnownLobby.Id, playerId, dataCurr, (r) => { onComplete?.Invoke(); }, null, null);
}
/// <summary>
/// Lobby can be provided info about Relay (or any other remote allocation) so it can add automatic disconnect handling.
/// </summary>
public void UpdatePlayerRelayInfoAsync(string allocationId, string connectionInfo, Action onComplete)
{
if (!ShouldUpdateData(() => { UpdatePlayerRelayInfoAsync(allocationId, connectionInfo, onComplete); }, onComplete, true)) // Do retry here since the RelayUtpSetup that called this might be destroyed right after this.
return;
string playerId = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
LobbyAPIInterface.UpdatePlayerAsync(m_lastKnownLobby.Id, playerId, new Dictionary<string, PlayerDataObject>(), (r) => { onComplete?.Invoke(); }, allocationId, connectionInfo);
if (!ShouldUpdateData(() => { UpdateLobbyDataAsync(data, onComplete); }, onComplete))
if (!ShouldUpdateData(() => { UpdateLobbyDataAsync(data, onComplete); }, onComplete, false))
return;
Lobby lobby = m_lastKnownLobby;

DataObject dataObj = new DataObject(visibility: DataObject.VisibilityOptions.Public, value: dataNew.Value); // Public so that when we request the list of lobbies, we can get info about them for filtering.
// 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 == "Color" ? 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.
private bool ShouldUpdateData(Action caller, Action onComplete)
/// <summary>
/// If we are in the middle of another operation, hold onto any pending ones until after that.
/// If we aren't in a lobby yet, leave it to the caller to decide what to do, since some callers might need to retry and others might not.
/// </summary>
private bool ShouldUpdateData(Action caller, Action onComplete, bool shouldRetryIfLobbyNull)
if (m_isMidRetrieve)
{ m_pendingOperations.Enqueue(caller);
if (m_rateLimitQuery.IsInCooldown)
{ m_rateLimitQuery.EnqueuePendingOperation(caller);
{ onComplete?.Invoke();
{
if (shouldRetryIfLobbyNull)
m_rateLimitQuery.EnqueuePendingOperation(caller);
onComplete?.Invoke();
}
private float m_heartbeatTime = 0;
private const float k_heartbeatPeriod = 8; // The heartbeat must be rate-limited to 5 calls per 30 seconds. We'll aim for longer in case periods don't align.
/// <summary>
/// Lobby requires a periodic ping to detect rooms that are still active, in order to mitigate "zombie" lobbies.
/// </summary>
public void DoLobbyHeartbeat(float dt)
{
m_heartbeatTime += dt;
if (m_heartbeatTime > k_heartbeatPeriod)
{
m_heartbeatTime -= k_heartbeatPeriod;
LobbyAPIInterface.HeartbeatPlayerAsync(m_lastKnownLobby.Id);
}
}
public class RateLimitCooldown : Observed<RateLimitCooldown>
{
private float m_timeSinceLastCall = float.MaxValue;
private readonly float m_cooldownTime;
private Queue<Action> m_pendingOperations = new Queue<Action>();
private bool m_isHandlingPending = false; // Just in case a pending operation tries to enqueue itself again.
public void EnqueuePendingOperation(Action action)
{
if (!m_isHandlingPending)
m_pendingOperations.Enqueue(action);
}
private bool m_isInCooldown = false;
public bool IsInCooldown
{
get => m_isInCooldown;
private set
{ if (m_isInCooldown != value)
{ m_isInCooldown = value;
OnChanged(this);
}
}
}
public RateLimitCooldown(float cooldownTime)
{
m_cooldownTime = cooldownTime;
}
public bool CanCall()
{
if (m_timeSinceLastCall < m_cooldownTime)
return false;
else
{
Locator.Get.UpdateSlow.Subscribe(OnUpdate, m_cooldownTime);
m_timeSinceLastCall = 0;
IsInCooldown = true;
return true;
}
}
private void OnUpdate(float dt)
{
m_timeSinceLastCall += dt;
m_isHandlingPending = false; // (Backup in case a pending operation hit an exception.)
if (m_timeSinceLastCall >= m_cooldownTime)
{
IsInCooldown = false;
if (!m_isInCooldown) // It's possible that by setting IsInCooldown, something called CanCall immediately, in which case we want to stay on UpdateSlow.
{
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); // Note that this is after IsInCooldown is set, to prevent an Observer from kicking off CanCall again immediately.
m_isHandlingPending = true;
while (m_pendingOperations.Count > 0)
m_pendingOperations.Dequeue()?.Invoke(); // Note: If this ends up enqueuing many operations, we might need to batch them and/or ensure they don't all execute at once.
m_isHandlingPending = false;
}
}
}
public override void CopyObserved(RateLimitCooldown oldObserved) { /* This behavior isn't needed; we're just here for the OnChanged event management. */ }
}
}
}

56
Assets/Scripts/Lobby/LobbyContentHeartbeat.cs


using System;
using System.Collections.Generic;
/// Keep updated on changes to a joined lobby.
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbyContentHeartbeat
{

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

m_shouldPushData = true;
}
public void OnUpdate(float dt)
/// <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>
private void OnUpdate(float dt)
if (m_localUser.IsHost)
LobbyAsyncRequests.Instance.DoLobbyHeartbeat(dt);
void PushDataToLobby()
{

void DoLobbyDataPush()
{
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(lobby.ToLocalLobby.RetrieveLobbyData(m_localLobby), () => { DoPlayerDataPush(); });
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(RetrieveLobbyData(m_localLobby), () => { DoPlayerDataPush(); });
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(lobby.ToLocalLobby.RetrieveUserData(m_localUser), () => { m_isAwaitingQuery = false; });
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(RetrieveUserData(m_localUser), () => { m_isAwaitingQuery = false; });
}
void OnRetrieve()

if (lobbyRemote == null) return;
bool prevShouldPush = m_shouldPushData;
var prevState = m_localLobby.State;
lobby.ToLocalLobby.Convert(lobbyRemote, m_localLobby, m_localUser);
lobby.ToLocalLobby.Convert(lobbyRemote, m_localLobby);
CheckForAllPlayersReady();
if (prevState != LobbyState.Lobby && m_localLobby.State == LobbyState.Lobby)
Locator.Get.Messenger.OnReceiveMessage(MessageType.ToLobby, null);
}
void CheckForAllPlayersReady()
{
bool areAllPlayersReady = m_localLobby.AllPlayersReadyTime != null;
if (areAllPlayersReady)
{
long targetTimeTicks = m_localLobby.AllPlayersReadyTime.Value;
DateTime targetTime = new DateTime(targetTimeTicks);
if (targetTime.Subtract(DateTime.Now).Seconds < 0)
return;
private static Dictionary<string, string> RetrieveLobbyData(LocalLobby lobby)
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add("RelayCode", lobby.RelayCode);
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());
return data;
}
Locator.Get.Messenger.OnReceiveMessage(MessageType.Client_EndReadyCountdownAt, targetTime); // Note that this could be called multiple times.
}
}
private static Dictionary<string, string> RetrieveUserData(LobbyUser user)
{
Dictionary<string, string> data = new Dictionary<string, string>();
if (user == null || string.IsNullOrEmpty(user.ID))
return data;
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());
return data;
}
}
}

67
Assets/Scripts/Lobby/ToLocalLobby.cs


/// <summary>
/// Create a new LocalLobby from the content of a retrieved lobby. Its data can be copied into an existing LocalLobby for use.
/// </summary>
public static void Convert(Lobby lobby, LocalLobby outputToHere, LobbyUser existingLocalUser = null)
public static void Convert(Lobby lobby, LocalLobby outputToHere)
LobbyInfo info = new LobbyInfo
LocalLobby.LobbyData info = new LocalLobby.LobbyData // Technically, this is largely redundant after the first assignment, but it won't do any harm to assign it again.
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null,
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null, // 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.
AllPlayersReadyTime = lobby.Data?.ContainsKey("AllPlayersReady") == true ? long.Parse(lobby.Data["AllPlayersReady"].Value) : (long?)null
Color = lobby.Data?.ContainsKey("Color") == true ? (LobbyColor) int.Parse(lobby.Data["Color"].Value) : LobbyColor.None
if (existingLocalUser != null && player.Id.Equals(existingLocalUser.ID))
// 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))
existingLocalUser.IsHost = lobby.HostId.Equals(player.Id);
existingLocalUser.DisplayName = player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : existingLocalUser.DisplayName;
existingLocalUser.Emote = player.Data?.ContainsKey("Emote") == true ? player.Data["Emote"].Value : existingLocalUser.Emote;
lobbyUsers.Add(existingLocalUser.ID, existingLocalUser);
if (status > (int)UserStatus.Connecting && outputToHere.LobbyUsers.ContainsKey(player.Id))
{
lobbyUsers.Add(player.Id, outputToHere.LobbyUsers[player.Id]);
continue;
}
else
// 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
LobbyUser user = new LobbyUser(
displayName: player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : "NewPlayer",
isHost: lobby.HostId.Equals(player.Id),
id: player.Id,
emote: player.Data?.ContainsKey("Emote") == true ? player.Data["Emote"].Value : null,
userStatus: player.Data?.ContainsKey("UserStatus") == true ? player.Data["UserStatus"].Value : UserStatus.Lobby.ToString()
);
lobbyUsers.Add(user.ID, user);
}
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);
/// Create a list of new LocalLobby from the content of a retrieved lobby.
/// Create a list of new LocalLobbies from the result of a lobby list query.
/// </summary>
public static List<LocalLobby> Convert(QueryResponse response)
{

private static LocalLobby Convert(Lobby lobby)
{
LocalLobby data = new LocalLobby();
Convert(lobby, data, null);
return data;
}
public static Dictionary<string, string> RetrieveLobbyData(LocalLobby lobby)
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add("RelayCode", lobby.RelayCode);
data.Add("State", ((int)lobby.State).ToString());
// We only want the ArePlayersReadyTime to be set when we actually are ready for it, and it's null otherwise. So, don't set that here.
return data;
}
public static Dictionary<string, string> RetrieveUserData(LobbyUser user)
{
Dictionary<string, string> data = new Dictionary<string, string>();
if (user == null || string.IsNullOrEmpty(user.ID))
return data;
data.Add("DisplayName", user.DisplayName);
data.Add("Emote", user.Emote); // Emote could be null, which is fine.
data.Add("UserStatus", user.UserStatus.ToString());
Convert(lobby, data);
return data;
}
}

5
Assets/Scripts/LobbyRelaySample.asmdef


"GUID:03058786646e84a4587858e9302c3f41",
"GUID:5540e30183c82e84b954c033c388e06c",
"GUID:fe25561d224ed4743af4c60938a59d0b",
"GUID:4c3f49d89436d478ea78315c03159dcc"
"GUID:4c3f49d89436d478ea78315c03159dcc",
"GUID:f2d49d9fa7e7eb3418e39723a7d3b92f"
"allowUnsafeCode": false,
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,

3
Assets/Scripts/Tests/Editor/AuthTests.cs


{
public class AuthTests
{
/// <summary>
/// Ensure that any changes to a flavor of SubIdentity are automatically updated.
/// </summary>
[Test]
public void IdentityBasicSubidentity()
{

38
Assets/Scripts/Tests/Editor/LoggerTests.cs


using LobbyRelaySample;
namespace LobbyRelaySample.Tests
namespace Test
/// <summary>
/// Tests of the LogHandler overriding debug logging.
/// </summary>
/// <summary>Reset the console between tests.</summary>
Debug.ClearDeveloperConsole(); // Reset Console between tests
Debug.ClearDeveloperConsole();
/// <summary>
/// Only display Log messages when set to Verbose.
/// </summary>
public void TestLog() // Should not show when not Verbose
public void TestLog()
LogAssert.NoUnexpectedReceived(); //Checks to see if there is anything here, there should not be
LogAssert.NoUnexpectedReceived(); // Ensure that we haven't received any unexpected logs.
LogHandler.Get().mode = LogMode.Warnings;
Debug.Log("WarningLog");

LogAssert.Expect(LogType.Log, "VerbLog");
}
/// <summary>
/// Only display Warning messages when set to Verbose or Warnings.
/// </summary>
public void TestWarning() // Should not show when Critical
public void TestWarning()
LogAssert.NoUnexpectedReceived(); //Checks to see if there is anything here, there should not be
LogAssert.NoUnexpectedReceived();
LogHandler.Get().mode = LogMode.Warnings;
Debug.LogWarning("WarningWarning");

LogAssert.Expect(LogType.Warning, "VerbWarning");
}
/// <summary>
/// Always display Error messages.
/// </summary>
public void TestError() // Should show regardless.
public void TestError()
{
LogHandler.Get().mode = LogMode.Critical;
Debug.LogError("CritError");

LogAssert.Expect(LogType.Error, "VerbError");
}
/// <summary>
/// Always display Assert messages.
/// </summary>
public void TestAssert() //Should Show regardless
public void TestAssert()
{
LogHandler.Get().mode = LogMode.Critical;
Debug.LogAssertion(true);

LogAssert.Expect(LogType.Assert, "True");
}
/// <summary>
/// Always display Exception messages.
/// </summary>
public void TestException() //Should Show regardless
public void TestException()
{
LogHandler.Get().mode = LogMode.Critical;
LogAssert.Expect(LogType.Exception, "Exception: CriticalException");

53
Assets/Scripts/Tests/Editor/MessengerTests.cs


using System.Text.RegularExpressions;
using UnityEngine.TestTools;
public class MessengerTests
namespace Test
private class Subscriber : IReceiveMessages
public class MessengerTests
private Action m_thingToDo;
public Subscriber(Action thingToDo)
/// <summary>Trivial message recipient that will run some action on any message.</summary>
private class Subscriber : IReceiveMessages
m_thingToDo = thingToDo;
private Action m_thingToDo;
public Subscriber(Action thingToDo) { m_thingToDo = thingToDo; }
public void OnReceiveMessage(MessageType type, object msg) { m_thingToDo?.Invoke(); }
public void OnReceiveMessage(MessageType type, object msg)
/// <summary>
/// If a message recipient takes a long time to process a message, we want to be made aware.
/// </summary>
[Test]
public void WhatIfAMessageIsVerySlow()
m_thingToDo?.Invoke();
}
}
[Test]
public void WhatIfAMessageIsVerySlow()
{
Messenger messenger = new Messenger();
int msgCount = 0;
string inefficientString = "";
Subscriber sub = new Subscriber(() =>
{ for (int n = 0; n < 12345; n++)
inefficientString += n.ToString();
msgCount++;
});
messenger.Subscribe(sub);
Messenger messenger = new Messenger();
int msgCount = 0;
string inefficientString = "";
Subscriber sub = new Subscriber(() =>
{ for (int n = 0; n < 12345; n++)
inefficientString += n.ToString();
msgCount++;
});
messenger.Subscribe(sub);
LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(".*took too long.*"));
messenger.OnReceiveMessage(MessageType.None, "");
LogAssert.Expect(UnityEngine.LogType.Warning, new Regex(".*took too long.*"));
messenger.OnReceiveMessage(MessageType.None, "");
Assert.AreEqual(1, msgCount, "Should have acted on the message.");
Assert.AreEqual(1, msgCount, "Should have acted on the message.");
}
}
}

22
Assets/Scripts/Tests/Editor/ObserverTests.cs


using LobbyRelaySample;
namespace LobbyRelaySample.Tests
namespace Test
/// <summary>
/// When an observed value changes, the Observer should automatically update.
/// </summary>
public IEnumerator ObserverChangeWhenObservedChanged() // Test if Observer changes when StringField gets set
public IEnumerator ObserverChangeWhenObservedChanged()
{
var observed = new TestObserved();
var observer = new GameObject("PlayerObserver").AddComponent<TestObserverBehaviour>();

Assert.AreEqual(observed.StringField, observer.displayStringField);
}
/// <summary>
/// When an Observer is registered, it should receive the observed field's initial value.
/// </summary>
/// <returns></returns>
public IEnumerator ObserverRegistersInitialChanges() // Test if Observer changes on Initialization
public IEnumerator ObserverRegistersInitialChanges()
observed.StringField = "NewName"; // Set the field before we begin observing
observed.StringField = "NewName";
var observer = new GameObject("PlayerObserver").AddComponent<TestObserverBehaviour>();
Assert.AreNotEqual(observed.StringField, observer.displayStringField);

Assert.AreEqual(observed.StringField, observer.displayStringField);
}
class TestObserved : Observed<TestObserved>
// We just have a couple Observers that update some arbitrary member, in this case a string.
private class TestObserved : Observed<TestObserved>
{
string m_stringField;

}
}
//Mock UI Observer
class TestObserverBehaviour : ObserverBehaviour<TestObserved>
private class TestObserverBehaviour : ObserverBehaviour<TestObserved>
{
public string displayStringField;

80
Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs


using NUnit.Framework;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;

namespace Test
{
/// <summary>
/// Hits the Authentication and Lobbies services in order to ensure lobbies can be created and deleted.
/// The actual code accessing lobbies should go through LobbyAsyncRequests.
/// Accesses the Authentication and Lobby services in order to ensure lobbies can be created and deleted.
/// LobbyAsyncRequests wraps the Lobby 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 Lobby service itself properly.
/// </summary>
public class LobbyRoundtripTests
{

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.
m_mockUserData = new Dictionary<string, PlayerDataObject>();
m_mockUserData.Add("DisplayName", new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "TestUser123"));
}
[UnityTearDown]

public void Teardown()
{
m_auth?.Dispose();
LogAssert.ignoreFailingMessages = false;
/// <summary>
/// Make sure the entire roundtrip for Lobby works: Once signed in, create a lobby, query to make sure it exists, then delete it.
/// </summary>
LogAssert.ignoreFailingMessages = true; // Not sure why, but when auth logs in, it sometimes generates an error: "A Native Collection has not been disposed[...]." We don't want this to cause test failures, since in practice it *seems* to not negatively impact behavior.
#region Setup
// Wait a reasonable amount of time for sign-in to complete.
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);

// 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.
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request, in case a previous test had one; Query requests can only occur at a rate of 1 per second.
Response<QueryResponse> queryResponse = null;
QueryResponse queryResponse = null;
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; });
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponse = qr; });
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#0)");
int numLobbiesIni = queryResponse.Result.Results?.Count ?? 0;
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#0)");
int numLobbiesIni = queryResponse.Results?.Count ?? 0;
#endregion
Response<Lobby> createResponse = null;
Lobby createResponse = null;
LobbyAPIInterface.CreateLobbyAsync(m_auth.GetContent("id"), lobbyName, 100, false, (r) => { createResponse = r; });
LobbyAPIInterface.CreateLobbyAsync(m_auth.GetContent("id"), lobbyName, 100, false, m_mockUserData, (r) => { createResponse = r; });
Assert.IsTrue(createResponse.Status >= 200 && createResponse.Status < 300, "CreateLobbyAsync should return a success code.");
m_workingLobbyId = createResponse.Result.Id;
Assert.AreEqual(lobbyName, createResponse.Result.Name, "Created lobby should match the provided name.");
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.");
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; });
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponse = qr; });
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#1)");
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Result.Results.Count, "Queried lobbies list should contain the test lobby.");
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Name == lobbyName).Count() == 1, "Checking queried lobby for name.");
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Id == m_workingLobbyId).Count() == 1, "Checking queried lobby for ID.");
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#1)");
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.");
Response<Lobby> getResponse = null;
Lobby getResponse = null;
LobbyAPIInterface.GetLobbyAsync(createResponse.Result.Id, (r) => { getResponse = r; });
LobbyAPIInterface.GetLobbyAsync(createResponse.Id, (r) => { getResponse = r; });
Assert.IsTrue(getResponse.Status >= 200 && getResponse.Status < 300, "GetLobbyAsync should return a success code.");
Assert.AreEqual(lobbyName, getResponse.Result.Name, "Checking the lobby we got for name.");
Assert.AreEqual(m_workingLobbyId, getResponse.Result.Id, "Checking the lobby we got for ID.");
Assert.IsNotNull(getResponse, "GetLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, getResponse.Name, "Checking the lobby we got for name.");
Assert.AreEqual(m_workingLobbyId, getResponse.Id, "Checking the lobby we got for ID.");
Response deleteResponse = null;
bool didDeleteFinish = false;
LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, (r) => { deleteResponse = r; });
while (deleteResponse == null && timeout > 0)
LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, () => { didDeleteFinish = true; });
while (timeout > 0 && !didDeleteFinish)
Assert.IsTrue(deleteResponse.Status >= 200 && deleteResponse.Status < 300, "DeleteLobbyAsync should return a success code.");
Response<QueryResponse> queryResponseTwo = null;
QueryResponse queryResponseTwo = null;
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponseTwo = qr; });
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponseTwo = qr; });
Assert.IsTrue(queryResponseTwo.Status >= 200 && queryResponseTwo.Status < 300, "QueryAllLobbiesAsync should return a success code. (#2)");
Assert.AreEqual(numLobbiesIni, queryResponseTwo.Result.Results.Count, "Queried lobbies list should be empty.");
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#2)");
Assert.AreEqual(numLobbiesIni, queryResponseTwo.Results.Count, "Queried lobbies list should be empty.");
LogAssert.ignoreFailingMessages = false;
/// <summary>
/// If the Lobby create call fails, we want to ensure we call onComplete so we can act on the failure.
/// </summary>
LogAssert.ignoreFailingMessages = true;
LogAssert.Expect(LogType.Exception, new Regex(".*400 Bad Request.*"));
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)

LobbyAPIInterface.CreateLobbyAsync("ThisStringIsInvalidHere", "lobby name", 123, false, (r) => { didComplete = (r == null); });
LobbyAPIInterface.CreateLobbyAsync("ThisStringIsInvalidHere", "lobby name", 123, false, m_mockUserData, (r) => { didComplete = (r == null); });
float timeout = 5;
while (didComplete == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);

99
Assets/Scripts/Tests/PlayMode/RelayRoundTripTests.cs


using System;
using System.Collections;
using LobbyRelaySample.Relay;
using LobbyRelaySample.relay;
using NUnit.Framework;
using Unity.Services.Relay;
using Unity.Services.Relay.Models;

namespace Test
{
/// <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;

public void Teardown()
{
m_auth?.Dispose();
LogAssert.ignoreFailingMessages = false;
/// <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>
LogAssert.ignoreFailingMessages = true;
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
//Allocation
float timeout = 5;
Response<AllocateResponseBody> allocationResponse = null;
RelayInterface.AllocateAsync(4, (a) => { allocationResponse = a; });
while (allocationResponse == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
Assert.Greater(timeout, 0, "Timeout Check (Allocate)");
Assert.IsTrue(allocationResponse.Status >= 200 && allocationResponse.Status < 300, "AllocationResponse should return a success code.");
Guid allocationId = allocationResponse.Result.Data.Allocation.AllocationId;
var allocationIP = allocationResponse.Result.Data.Allocation.RelayServer.IpV4;
var allocationPort = allocationResponse.Result.Data.Allocation.RelayServer.Port;
Assert.NotNull(allocationId);
Assert.NotNull(allocationIP);
Assert.NotNull(allocationPort);
//Join Code Fetch
timeout = 5;
Response<JoinCodeResponseBody> joinCodeResponse = null;
RelayInterface.GetJoinCodeAsync(allocationId, (j) => { joinCodeResponse = j; });
while (joinCodeResponse == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
Assert.Greater(timeout, 0, "Timeout Check (JoinCode)");
Assert.IsTrue(allocationResponse.Status >= 200 && allocationResponse.Status < 300, "JoinCodeResponse should return a success code.");
string joinCode = joinCodeResponse.Result.Data.JoinCode;
Assert.False(string.IsNullOrEmpty(joinCode));
//Join Via Join Code
timeout = 5;
Response<JoinResponseBody> joinResponse = null;
RelayInterface.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)");
Assert.IsTrue(allocationResponse.Status >= 200 && allocationResponse.Status < 300, "JoinResponse should return a success code.");
var codeIp = joinResponse.Result.Data.Allocation.RelayServer.IpV4;
var codePort = joinResponse.Result.Data.Allocation.RelayServer.Port;
Assert.AreEqual(codeIp, allocationIP);
Assert.AreEqual(codePort, allocationPort);
}
[UnityTest]
public IEnumerator DoShortcutRoundtrip()
{
LogAssert.ignoreFailingMessages = true;
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)

//Allocation
// Allocation
RelayInterface.AllocateAsync(4, (a) => { allocation = a; });
RelayAPIInterface.AllocateAsync(4, (a) => { allocation = a; });
while (allocation == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);

Assert.NotNull(allocationIP);
Assert.NotNull(allocationPort);
//Join Code Fetch
// Join code retrieval
RelayInterface.GetJoinCodeAsync(allocationId, (j) => { joinCode = j; });
RelayAPIInterface.GetJoinCodeAsync(allocationId, (j) => { joinCode = j; });
while (joinCode == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);

Assert.Greater(timeout, 0, "Timeout Check (JoinCode)");
Assert.False(string.IsNullOrEmpty(joinCode));
//Join Via Join Code
// Joining with the join code
Response<JoinResponseBody> joinResponse = null;
RelayInterface.JoinAsync(joinCode, (j) => { joinResponse = j; });
JoinAllocation joinResponse = null;
RelayAPIInterface.JoinAsync(joinCode, (j) => { joinResponse = j; });
while (joinResponse == null && timeout > 0)
{
yield return new WaitForSeconds(0.25f);

Assert.Greater(timeout, 0, "Timeout Check (Join)");
var codeIp = joinResponse.Result.Data.Allocation.RelayServer.IpV4;
var codePort = joinResponse.Result.Data.Allocation.RelayServer.Port;
var codeIp = joinResponse.RelayServer.IpV4;
var codePort = joinResponse.RelayServer.Port;
Assert.AreEqual(codeIp, allocationIP);
Assert.AreEqual(codePort, allocationPort);
}

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


using UnityEngine.TestTools;
namespace Test
{
{
/// <summary>
/// Testing some edge cases with the UpdateSlow.
/// </summary>
private const float k_period = 1.5f;
/// <summary>Trivial Subscriber to do some action every UpdateSlow.</summary>
public float prevDt;
public Subscriber(Action thingToDo)
public Subscriber(Action thingToDo, float period)
Locator.Get.UpdateSlow.Subscribe(OnUpdate);
Locator.Get.UpdateSlow.Subscribe(OnUpdate, period);
m_thingToDo = thingToDo;
}

private void OnUpdate(float dt)
{
m_thingToDo?.Invoke();
prevDt = dt;
}
}

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

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

12
Assets/Scripts/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;
/// clients will actually wait for a message from the host confirming that they are in the game, instead of assuming the game is ready to go when the countdown ends.
public class CountdownUI : ObserverPanel<LocalLobby>
public class CountdownUI : LocalLobbyObserver
public override void ObservedUpdated(LocalLobby observed)
protected override void UpdateObserver(LocalLobby obs)
base.UpdateObserver(obs);
return;
m_CountDownText.SetText($"Starting in: {observed.CountDownTime}");
m_CountDownText.SetText("Waiting for all players...");
else
m_CountDownText.SetText($"Starting in: {observed.CountDownTime:0}");
}
}
}

4
Assets/Scripts/UI/EmoteButtonUI.cs


public class EmoteButtonUI : MonoBehaviour
{
[SerializeField]
TMP_Text m_EmoteTextElement;
private EmoteType m_emoteType;
Locator.Get.Messenger.OnReceiveMessage(MessageType.UserSetEmote, m_EmoteTextElement.text);
Locator.Get.Messenger.OnReceiveMessage(MessageType.UserSetEmote, m_emoteType);
}
}
}

2
Assets/Scripts/UI/EndGameButtonUI.cs


{
public void EndGame()
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.ToLobby, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
}
}
}

19
Assets/Scripts/UI/InLobbyUserUI.cs


using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace LobbyRelaySample.UI
{

[SerializeField]
TMP_Text m_EmoteText;
[SerializeField]
Image m_HostIcon;
public bool IsAssigned
{

{
m_DisplayNameText.SetText(observed.DisplayName);
m_StatusText.SetText(SetStatusFancy(observed.UserStatus));
m_EmoteText.SetText(observed.Emote);
m_EmoteText.SetText(observed.Emote.GetString());
m_HostIcon.enabled = observed.IsHost;
}
string SetStatusFancy(UserStatus status)

case UserStatus.Lobby:
return "<color=#56B4E9>Lobby.</color>"; // Light Blue
return "<color=#56B4E9>In Lobby</color>"; // Light Blue
return "<color=#009E73>Ready!</color>"; // Light Mint
return "<color=#009E73>Ready</color>"; // Light Mint
return "<color=#F0E442>Connecting.</color>"; // Bright Yellow
case UserStatus.Connected:
return "<color=#005500>Connected.</color>"; //Orange
return "<color=#F0E442>Connecting...</color>"; // Bright Yellow
case UserStatus.InGame:
return "<color=#005500>In Game</color>"; // Green
return "<color=#56B4E9>In Lobby.</color>";
return "";
}
}
}

1
Assets/Scripts/UI/JoinCreateLobbyUI.cs


if (observed.State == GameState.JoinMenu)
{
Show();
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null);
}
else
{

24
Assets/Scripts/UI/JoinMenuUI.cs


using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

LobbyButtonUI m_LobbyButtonPrefab;
[SerializeField]
TMP_InputField m_LobbyCodeField;
[SerializeField]
RectTransform m_LobbyButtonParent;
/// <summary>

Dictionary<string, LocalLobby> m_LocalLobby = new Dictionary<string, LocalLobby>();
/// <summary>Contains some amount of information used to join an existing lobby.</summary>
LobbyInfo m_LocalLobbySelected;
LocalLobby.LobbyData m_LocalLobbySelected;
public void LobbyButtonSelected(LocalLobby lobby)
{

public void OnJoinCodeInputFieldChanged(string newCode)
public void OnLobbyCodeInputFieldChanged(string newCode)
m_LocalLobbySelected = new LobbyInfo(newCode.ToUpper());
m_LocalLobbySelected = new LocalLobby.LobbyData(newCode.ToUpper());
}
public void OnJoinButtonPressed()

RemoveLobbyButton(m_LocalLobby[key]);
}
bool CanDisplay(LocalLobby lobby)
public void JoinMenuChangedVisibility(bool show)
{
if (show)
OnRefresh();
}
private bool CanDisplay(LocalLobby lobby)
{
return lobby.Data.State == LobbyState.Lobby && !lobby.Private;
}

/// </summary>
void AddNewLobbyButton(string lobbyCode, LocalLobby lobby)
private void AddNewLobbyButton(string lobbyCode, LocalLobby lobby)
{
var lobbyButtonInstance = Instantiate(m_LobbyButtonPrefab, m_LobbyButtonParent);
lobbyButtonInstance.GetComponent<LocalLobbyObserver>().BeginObserving(lobby);

m_LocalLobby.Add(lobbyCode, lobby);
}
void UpdateLobbyButton(string lobbyCode, LocalLobby lobby)
private void UpdateLobbyButton(string lobbyCode, LocalLobby lobby)
void RemoveLobbyButton(LocalLobby lobby)
private void RemoveLobbyButton(LocalLobby lobby)
{
var lobbyID = lobby.LobbyID;
var lobbyButton = m_LobbyButtons[lobbyID];

2
Assets/Scripts/UI/ReadyCheckUI.cs


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

15
Assets/Scripts/UI/SpinnerUI.cs


public override void ObservedUpdated(LobbyServiceData observed)
{
if (observed.State == LobbyServiceState.Fetching)
if (observed.State == LobbyQueryState.Fetching)
{
Show();
spinnerImage.Show();

else if (observed.State == LobbyServiceState.Error)
else if (observed.State == LobbyQueryState.Error)
var errorString = new StringBuilder();
errorString.Append("Error");
var codeString = ": " + observed.lastErrorCode;
if (observed.lastErrorCode < 1)
codeString = ".";
errorString.Append(codeString);
errorText.SetText(errorString.ToString());
errorText.SetText("Error. See Unity Console log for details.");
else if (observed.State == LobbyServiceState.Fetched)
else if (observed.State == LobbyQueryState.Fetched)
{
if (observed.CurrentLobbies.Count < 1)
{

24
Assets/Scripts/UI/UIPanelBase.cs


using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

{
[SerializeField]
private UnityEvent<bool> m_onVisibilityChange;
[SerializeField]
List<UIPanelBase> m_uiPanelsInChildren = new List<UIPanelBase>(); // Otherwise, when this Shows/Hides, the children won't know to update their own visibility.
public void Start()
{
var children = GetComponentsInChildren<UIPanelBase>(true); // Note that this won't detect children in GameObjects added during gameplay, if there were any.
foreach (var child in children)
if (child != this)
m_uiPanelsInChildren.Add(child);
}
protected CanvasGroup MyCanvasGroup
{

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

5
Assets/Scripts/Game/ServerAddress.cs


using UnityEngine;
/// This is where your netcode would go, if you had it.
/// Just for displaying the anonymous Relay IP.
/// </summary>
public class ServerAddress
{

{
m_IP = ip;
m_Port = port;
Debug.Log($"Connected To Game Server: {ip}:{port}");
}
public override string ToString()

136
Assets/Scripts/Game/LocalLobby.cs


using System;
using System.Collections.Generic;
using System.Linq;
[Flags]
[Flags] // Some UI elements will want to specify multiple states in which to be active, so this is Flags.
public enum LobbyState
{
Lobby = 1,

public struct LobbyInfo
{
public string LobbyID { get; set; }
public string LobbyCode { get; set; }
public string RelayCode { get; set; }
public string LobbyName { get; set; }
public bool Private { get; set; }
public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public long? AllPlayersReadyTime { get; set; }
public LobbyInfo(LobbyInfo existing)
{
LobbyID = existing.LobbyID;
LobbyCode = existing.LobbyCode;
RelayCode = existing.RelayCode;
LobbyName = existing.LobbyName;
Private = existing.Private;
MaxPlayerCount = existing.MaxPlayerCount;
State = existing.State;
AllPlayersReadyTime = existing.AllPlayersReadyTime;
}
public LobbyInfo(string lobbyCode)
{
LobbyID = null;
LobbyCode = lobbyCode;
RelayCode = null;
LobbyName = null;
Private = false;
MaxPlayerCount = -1;
State = LobbyState.Lobby;
AllPlayersReadyTime = null;
}
}
public enum LobbyColor { None = 0, Orange = 1, Green = 2, Blue = 3 }
/// (The way that the Lobby service handles its data doesn't necessarily match our needs, so we need to map from that to this LocalLobby for use in the sample code.)
/// </summary>
[System.Serializable]
public class LocalLobby : Observed<LocalLobby>

#region LocalLobbyData
private LobbyInfo m_data;
public LobbyInfo Data
public struct LobbyData
get { return new LobbyInfo(m_data); }
}
public string LobbyID { get; set; }
public string LobbyCode { get; set; }
public string RelayCode { get; set; }
public string LobbyName { get; set; }
public bool Private { get; set; }
public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public LobbyColor Color { get; set; }
float m_CountDownTime;
public LobbyData(LobbyData existing)
{
LobbyID = existing.LobbyID;
LobbyCode = existing.LobbyCode;
RelayCode = existing.RelayCode;
LobbyName = existing.LobbyName;
Private = existing.Private;
MaxPlayerCount = existing.MaxPlayerCount;
State = existing.State;
Color = existing.Color;
}
public float CountDownTime
{
get { return m_CountDownTime; }
set
public LobbyData(string lobbyCode)
m_CountDownTime = value;
OnChanged(this);
LobbyID = null;
LobbyCode = lobbyCode;
RelayCode = null;
LobbyName = null;
Private = false;
MaxPlayerCount = -1;
State = LobbyState.Lobby;
Color = LobbyColor.None;
DateTime m_TargetEndTime;
private LobbyData m_data;
public LobbyData Data
{
get { return new LobbyData(m_data); }
}
float m_CountDownTime;
public DateTime TargetEndTime
public float CountDownTime
get => m_TargetEndTime;
get { return m_CountDownTime; }
m_TargetEndTime = value;
m_CountDownTime = value;
OnChanged(this);
}
}

/// <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;

}
}
public long? AllPlayersReadyTime => m_data.AllPlayersReadyTime;
/// <summary>
/// Checks if we have n players that have the Status.
/// -1 Count means you need all Lobbyusers
/// </summary>
/// <returns>True if enough players are of the input status.</returns>
public bool PlayersOfState(UserStatus status, int playersCount = -1)
public LobbyColor Color
var statePlayers = m_LobbyUsers.Values.Count(user => user.UserStatus == status);
if (playersCount < 0)
return statePlayers == m_LobbyUsers.Count;
return statePlayers == playersCount;
get => m_data.Color;
set
{
m_data.Color = value;
OnChanged(this);
}
public void CopyObserved(LobbyInfo info, Dictionary<string, LobbyUser> oldUsers)
public void CopyObserved(LobbyData data, Dictionary<string, LobbyUser> currUsers)
m_data = info;
if (oldUsers == null)
m_data = data;
if (currUsers == null)
foreach (var user in m_LobbyUsers)
foreach (var oldUser in m_LobbyUsers)
if (oldUsers.ContainsKey(user.Key))
user.Value.CopyObserved(oldUsers[user.Key]);
if (currUsers.ContainsKey(oldUser.Key))
oldUser.Value.CopyObserved(currUsers[oldUser.Key]);
toRemove.Add(user.Value);
toRemove.Add(oldUser.Value);
}
foreach (var remove in toRemove)

foreach (var oldUser in oldUsers)
foreach (var currUser in currUsers)
if (!m_LobbyUsers.ContainsKey(oldUser.Key))
DoAddPlayer(oldUser.Value);
if (!m_LobbyUsers.ContainsKey(currUser.Key))
DoAddPlayer(currUser.Value);
}
}

// This ends up being called from the lobby list when we get data about a lobby without having joined it yet.
public override void CopyObserved(LocalLobby oldObserved)
{
CopyObserved(oldObserved.Data, oldObserved.m_LobbyUsers);

15
Assets/Scripts/Game/LocalGameState.cs


namespace LobbyRelaySample
{
/// <summary>
/// Current state of the local Game.
/// Set as a flag to allow for the unity inspector to select multiples for various UI features.
/// 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

}
/// <summary>
/// Awaits player input to change the local game Data
/// Awaits player input to change the local game data.
/// </summary>
[System.Serializable]
public class LocalGameState : Observed<LocalGameState>

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

124
Assets/Scripts/Game/LobbyUser.cs


[Flags]
public enum UserStatus
{
Lobby = 1, // Connected to lobby, not ready yet
Ready = 4, // User clicked ready (Note that 2 is missing; some flags have been removed over time, but we want any serialized values to be unaffected.)
Connecting = 8, // User sent join request through Relay
Connected = 16, // User connected through Relay
Menu = 32, // User is in a menu, external to the lobby
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>

public class LobbyUser : Observed<LobbyUser>
{
public LobbyUser(bool isHost = false, string displayName = null, string id = null, string emote = null, string userStatus = null)
public LobbyUser(bool isHost = false, string displayName = null, string id = null, EmoteType emote = EmoteType.None, UserStatus userStatus = UserStatus.Menu)
m_isHost = isHost;
m_DisplayName = displayName;
m_ID = id;
m_Emote = emote;
UserStatus status;
if (!string.IsNullOrEmpty(userStatus) && Enum.TryParse(userStatus, out status))
m_UserStatus = status;
m_data = new UserData(isHost, displayName, id, emote, userStatus);
bool m_isHost;
#region Local UserData
public struct UserData
{
public bool IsHost { get; set; }
public string DisplayName { get; set; }
public string ID { get; set; }
public EmoteType Emote { get; set; }
public UserStatus UserStatus { get; set; }
public UserData(bool isHost, string displayName, string id, EmoteType emote, UserStatus userStatus)
{
IsHost = isHost;
DisplayName = displayName;
ID = id;
Emote = emote;
UserStatus = userStatus;
}
}
private UserData m_data;
public void ResetState()
{
m_data = new UserData(false, m_data.DisplayName, m_data.ID, EmoteType.None, UserStatus.Menu); // ID and DisplayName should persist since this might be the local user.
}
#endregion
/// <summary>
/// Used for limiting costly OnChanged actions to just the members which actually changed.
/// </summary>
[Flags]
public enum UserMembers { IsHost = 1, DisplayName = 2, Emote = 4, ID = 8, UserStatus = 16 }
private UserMembers m_lastChanged;
public UserMembers LastChanged => m_lastChanged;
get { return m_isHost; }
get { return m_data.IsHost; }
if (m_isHost != value)
if (m_data.IsHost != value)
m_isHost = value;
m_data.IsHost = value;
m_lastChanged = UserMembers.IsHost;
string m_DisplayName = "";
get => m_DisplayName;
get => m_data.DisplayName;
if (m_DisplayName != value)
if (m_data.DisplayName != value)
m_DisplayName = value;
m_data.DisplayName = value;
m_lastChanged = UserMembers.DisplayName;
string m_Emote = "";
public string Emote
public EmoteType Emote
get => m_Emote;
get => m_data.Emote;
if (m_Emote != value)
if (m_data.Emote != value)
m_Emote = value;
m_data.Emote = value;
m_lastChanged = UserMembers.Emote;
string m_ID = "";
get => m_ID;
get => m_data.ID;
if (m_ID != value)
if (m_data.ID != value)
m_ID = value;
m_data.ID = value;
m_lastChanged = UserMembers.ID;
UserStatus m_UserStatus = UserStatus.Menu;
UserStatus m_userStatus = UserStatus.Menu;
get => m_UserStatus;
get => m_userStatus;
m_UserStatus = value;
OnChanged(this);
if (m_userStatus != value)
{
m_userStatus = value;
m_lastChanged = UserMembers.UserStatus;
OnChanged(this);
}
public override void CopyObserved(LobbyUser oldObserved)
public override void CopyObserved(LobbyUser observed)
m_DisplayName = oldObserved.m_DisplayName;
m_Emote = oldObserved.m_Emote;
m_ID = oldObserved.m_ID;
m_isHost = oldObserved.m_isHost;
m_UserStatus = oldObserved.m_UserStatus;
UserData data = observed.m_data;
int lastChanged = // Set flags just for the members that will be changed.
(m_data.DisplayName == data.DisplayName ? 0 : (int)UserMembers.DisplayName) |
(m_data.Emote == data.Emote ? 0 : (int)UserMembers.Emote) |
(m_data.ID == data.ID ? 0 : (int)UserMembers.ID) |
(m_data.IsHost == data.IsHost ? 0 : (int)UserMembers.IsHost) |
(m_data.UserStatus == data.UserStatus ? 0 : (int)UserMembers.UserStatus);
if (lastChanged == 0) // Ensure something actually changed.
return;
m_data = data;
m_lastChanged = (UserMembers)lastChanged;
OnChanged(this);
}
}

14
Assets/Scripts/Game/LobbyServiceData.cs


namespace LobbyRelaySample
{
public enum LobbyServiceState
/// <summary>
/// Used when displaying the lobby list, to indicate when we are awaiting an updated lobby query.
/// </summary>
public enum LobbyQueryState
{
Empty,
Fetching,

[System.Serializable]
public class LobbyServiceData : Observed<LobbyServiceData>
{
LobbyServiceState m_CurrentState = LobbyServiceState.Empty;
LobbyQueryState m_CurrentState = LobbyQueryState.Empty;
public long lastErrorCode;
public LobbyServiceState State
public LobbyQueryState State
{
get { return m_CurrentState; }
set

Dictionary<string, LocalLobby> m_currentLobbies = new Dictionary<string, LocalLobby>();
/// <summary>
/// Will only trigger if the dictionary is set wholesale. Changes in the size, or contents will not trigger OnChanged
/// string is lobby ID, Key is the Lobby data representation of it
/// 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
{

19
Packages/manifest.json


{
"dependencies": {
"com.unity.2d.animation": "6.0.3",
"com.unity.2d.pixel-perfect": "5.0.0",
"com.unity.2d.psdimporter": "6.0.0-pre.2",
"com.unity.2d.sprite": "1.0.0",
"com.unity.2d.spriteshape": "7.0.0-pre.2",
"com.unity.2d.tilemap": "1.0.0",
"com.unity.collab-proxy": "1.5.7",
"com.unity.collab-proxy": "1.7.1",
"com.unity.services.authentication": "1.0.0-pre.4",
"com.unity.services.core": "1.0.0",
"com.unity.services.lobby": "1.0.0-pre.4",
"com.unity.services.relay": "1.0.1-pre.1",
"com.unity.test-framework": "1.1.26",
"com.unity.test-framework": "1.1.27",
"com.unity.timeline": "1.6.0-pre.5",
"com.unity.transport": "1.0.0-pre.1",
"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": []
}

164
Packages/packages-lock.json


{
"dependencies": {
"com.unity.2d.animation": {
"version": "6.0.3",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.2d.common": "5.0.0",
"com.unity.mathematics": "1.1.0",
"com.unity.2d.sprite": "1.0.0",
"com.unity.modules.animation": "1.0.0",
"com.unity.modules.uielements": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.2d.common": {
"version": "6.0.0-pre.2",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.2d.sprite": "1.0.0",
"com.unity.mathematics": "1.1.0",
"com.unity.modules.uielements": "1.0.0",
"com.unity.burst": "1.5.1"
},
"url": "https://packages.unity.com"
},
"com.unity.2d.path": {
"version": "5.0.0",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.2d.pixel-perfect": {
"version": "5.0.0",
"depth": 0,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.2d.psdimporter": {
"version": "6.0.0-pre.2",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.2d.animation": "6.0.1",
"com.unity.2d.common": "6.0.0-pre.2",
"com.unity.2d.sprite": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.2d.sprite": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.2d.spriteshape": {
"version": "7.0.0-pre.2",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.mathematics": "1.1.0",
"com.unity.2d.common": "6.0.0-pre.2",
"com.unity.2d.path": "5.0.0",
"com.unity.modules.physics2d": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.2d.tilemap": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"version": "1.6.0-pre.2",
"version": "1.5.3",
"depth": 2,
"source": "registry",
"dependencies": {

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

},
"com.unity.collections": {
"version": "1.0.0-pre.3",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.burst": "1.5.3",
"com.unity.test-framework": "1.1.22"
},
"url": "https://packages.unity.com"
},
"com.unity.ext.nunit": {
"version": "1.0.6",
"depth": 1,

"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.services.authentication": {
"version": "1.0.0-pre.4",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.core": "1.1.0-pre.8",
"com.unity.modules.unitywebrequest": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.services.core": {
"version": "1.1.0-pre.8",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.modules.unitywebrequest": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.services.lobby": {
"version": "1.0.0-pre.4",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.services.core": "1.1.0-pre.8",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
"com.unity.modules.unitywebrequestaudio": "1.0.0",
"com.unity.modules.unitywebrequesttexture": "1.0.0",
"com.unity.modules.unitywebrequestwww": "1.0.0",
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.authentication": "1.0.0-pre.4"
},
"url": "https://packages.unity.com"
},
"com.unity.services.relay": {
"version": "1.0.1-pre.1",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.services.core": "1.1.0-pre.8",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
"com.unity.modules.unitywebrequestaudio": "1.0.0",
"com.unity.modules.unitywebrequesttexture": "1.0.0",
"com.unity.modules.unitywebrequestwww": "1.0.0",
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.authentication": "1.0.0-pre.4"
},
"url": "https://packages.unity.com"
},
"com.unity.sysroot": {
"version": "0.1.19-preview",
"depth": 1,

"url": "https://packages.unity.com"
},
"com.unity.test-framework": {
"version": "1.1.26",
"version": "1.1.27",
"depth": 0,
"source": "registry",
"dependencies": {

},
"url": "https://packages.unity.com"
},
"com.unity.timeline": {
"version": "1.6.0-pre.5",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.modules.director": "1.0.0",
"com.unity.modules.animation": "1.0.0",
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.particlesystem": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.toolchain.win-x86_64-linux-x86_64": {
"version": "0.1.20-preview",
"depth": 0,

"com.unity.sysroot.linux-x86_64": "0.1.14-preview"
},
"url": "https://packages.unity.com"
},
"com.unity.transport": {
"version": "file:com.unity.transport",
"depth": 0,
"source": "embedded",
"dependencies": {
"com.unity.collections": "1.0.0-pre.3",
"com.unity.burst": "1.5.1",
"com.unity.mathematics": "1.2.1"
}
},
"com.unity.ugui": {
"version": "1.0.0",

22
ProjectSettings/PackageManagerSettings.asset


m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier:
m_EnablePreReleasePackages: 0
m_EnablePackageDependencies: 0
m_EnablePreviewPackages: 1
m_EnablePackageDependencies: 1
m_SeeAllPackageVersions: 0
oneTimeWarningShown: 1
m_Registries:
- m_Id: main

m_UserSelectedRegistryName:
m_UserAddingNewScopedRegistry: 0
m_RegistryInfoDraft:
m_Modified: 0
m_UserModificationsInstanceId: -822
m_OriginalInstanceId: -824
m_LoadAssets: 0
m_Original:
m_Id:
m_Name:
m_Url:
m_Scopes: []
m_IsDefault: 0
m_Capabilities: 0
m_Modified: 0
m_Name:
m_Url:
m_Scopes:
-
m_SelectedScopeIndex: 0

20
ProjectSettings/ProjectSettings.asset


--- !u!129 &1
PlayerSettings:
m_ObjectHideFlags: 0
serializedVersion: 23
serializedVersion: 22
productGUID: 5cdb00278111a2a4885ff1a3fcb6c043
AndroidProfiler: 0
AndroidFilterTouchesWhenObscured: 0

androidRenderOutsideSafeArea: 1
androidUseSwappy: 1
androidBlitType: 0
androidResizableWindow: 0
androidDefaultWindowWidth: 1920
androidDefaultWindowHeight: 1080
androidMinimumWindowWidth: 400
androidMinimumWindowHeight: 300
androidFullscreenMode: 1
defaultIsNativeResolution: 1
macRetinaSupport: 1
runInBackground: 1

vulkanEnableSetSRGBWrite: 0
vulkanEnablePreTransform: 0
vulkanEnableLateAcquireNextImage: 0
vulkanEnableCommandBufferRecycling: 1
m_SupportedAspectRatios:
4:3: 1
5:4: 1

iOSLaunchScreeniPadCustomStoryboardPath:
iOSDeviceRequirements: []
iOSURLSchemes: []
macOSURLSchemes: []
iOSBackgroundModes: 0
iOSMetalForceHardShadows: 0
metalEditorSupport: 1

useCustomGradlePropertiesTemplate: 0
useCustomProguardFile: 0
AndroidTargetArchitectures: 1
AndroidTargetDevices: 0
AndroidSplashScreenScale: 0
androidSplashScreen: {fileID: 0}
AndroidKeystoreName:

height: 180
banner: {fileID: 0}
androidGamepadSupportLevel: 0
chromeosInputEmulation: 1
AndroidMinifyWithR8: 0
AndroidMinifyRelease: 0
AndroidMinifyDebug: 0

m_BuildTargetGroupLightmapEncodingQuality: []
m_BuildTargetGroupLightmapSettings: []
m_BuildTargetNormalMapEncoding: []
m_BuildTargetDefaultTextureCompressionFormat: []
playModeTestRunnerEnabled: 0
runPlayModeTestAsEditModeTest: 0
actionOnDotNetUnhandledException: 1

cameraUsageDescription:
locationUsageDescription:
microphoneUsageDescription:
bluetoothUsageDescription:
switchNMETAOverride:
switchNetLibKey:
switchSocketMemoryPoolSize: 6144

switchUseCPUProfiler: 0
switchUseGOLDLinker: 0
switchLTOSetting: 0
switchApplicationID: 0x01004b9000490000
switchNSODependencies:
switchTitleNames_0:

ps4videoRecordingFeaturesUsed: 0
ps4contentSearchFeaturesUsed: 0
ps4CompatibilityPS5: 0
ps4AllowPS5Detection: 0
ps4GPU800MHz: 1
ps4attribEyeToEyeDistanceSettingVR: 0
ps4IncludedModules: []

suppressCommonWarnings: 1
allowUnsafeCode: 0
useDeterministicCompilation: 1
useReferenceAssemblies: 1
enableRoslynAnalyzers: 1
additionalIl2CppArgs:
scriptingRuntimeVersion: 1

organizationId:
cloudEnabled: 0
legacyClampBlendShapeWeights: 0
playerDataPath:
forceSRGBBlit: 1
uploadClearedTextureDataAfterCreationFromScript: 0

4
ProjectSettings/ProjectVersion.txt


m_EditorVersion: 2021.2.0b1
m_EditorVersionWithRevision: 2021.2.0b1 (b0978dae4864)
m_EditorVersion: 2020.3.15f2
m_EditorVersionWithRevision: 2020.3.15f2 (6cf78cb77498)

2
ProjectSettings/UnityConnectSettings.asset


UnityConnectSettings:
m_ObjectHideFlags: 0
serializedVersion: 1
m_Enabled: 1
m_Enabled: 0
m_TestMode: 0
m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events
m_EventUrl: https://cdp.cloud.unity3d.com/v1/events

157
README.md


# Game Lobby Sample
_Tested with Unity 2020.3 for PC and Mac._
### Closed Beta - 7/14/21
Lobby and Relay are **only** available in closed beta at the moment, to use these services you will need to have signed up here for the services to show in your Organization: https://create.unity3d.com/relay-lobby-beta-signup
This sample demonstrates how to use the Lobby and Relay packages to create a typical game lobby experience. Players can host lobbies that other players can join using a public lobby list or lobby code, and then connect with Relay to use Unity Transport ("UTP") for basic real-time communication between them. Relay allows players to securely communicate with each other while maintaining connection anonymity.
**Note**: This is not a “drag-and-drop” solution; the Game Lobby Sample is not a minimal code sample intended to be completely copied into a full-scale project. Rather, it demonstrates how to use multiple services in a vertical slice with some basic game logic and infrastructure. Use it as a reference and starting point to learn how Lobby and Relay work together and how to integrate them into your project.
#### Features:
* **Anonymous Auth login**: Track player credentials without a persistent account.
* **Lobby creation**: Players host lobbies for others to join.
* **Lobby query**: Find a list of lobbies with filters, or use lobby codes.
* **Relay obfuscation**: Players in a lobby are connected through an anonymous IP.
* **UTP communication**: Players transmit basic data to lobby members in real time.
* **Lobby + Relay connection management**: Together, the services automatically handle new connections and disconnections.
### Service organization setup
To use Unity’s multiplayer services, you need a cloud organization ID for your project. If you do not currently have one, follow the **How do I create a new Organization?** article to set up your cloud organization:
[https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-](https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-)
Once you have an ID, link it to your project under **Edit **>** Project Settings **>** Services** and use the Unity Dashboard to manage your project’s services.
### Service overview
#### **Lobby**
The Lobby service allows developers to create lobbies and share data between players before a real-time network connection is established. It simplifies the first step in connecting users to other services such as Relay and provides tools to allow players to find other lobbies.
The Lobby documentation contains code samples and additional information about the service. It includes comprehensive details for using Lobby along with additional code samples, and it might help you better understand the Game Lobby Sample: [http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service)
The Lobby service can be managed in the Unity Dashboard:
# Game Lobby Sample
## *Unity 2021.2 0b1*
[https://dashboard.unity3d.com/lobby](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service)
This is a Unity Project Sample showing how to integrate Lobby and Relay into a typical Game Lobby experience.
Features Covered:
- Lobby Creation
- Lobby Query
- Lobby Data Sync
- Emotes
- Player Names
- Player Ready Check State
- Lobby Join
- Relay Server Creation
- Relay Code Generation
- Relay Server Join
#### **Relay**
## Service Organization Setup
**Create an organization**
The Relay service connects players in a host-client model with an obfuscated host IP. This allows players to host networked experiences as though players connected directly while only sharing private information with Relay itself. \
Follow the guide to set up your cloud organization:
[Organization Tutorial](https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-)
The Relay documentation contains code samples and additional information about the service. It includes comprehensive details for using Relay along with additional code samples, and it might help you better understand the Game Lobby Sample:
[http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview)
## Lobby & Relay
The Relay service can be managed in the Unity Dashboard:
We use the lobby service to create a space that our users can join and share data through.
[https://dashboard.unity3d.com/relay](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview)
[Lobby Overview](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service)
In this sample, once players are connected to a lobby, they are connected through Relay to set up real-time data transfer over UTP. Lobby and Relay both depend on Auth for credentials. This sample uses Auth’s anonymous login feature to create semi-permanent credentials that are unique to each player but do not require developers to maintain a persistent account for them.
[Lobby Dashboard](https://dashboard.unity3d.com/lobby)
#### **Setup**
The Lobby and Relay sections of the Unity Dashboard contain their own setup instructions. Select **About & Support **>** Get Started** and follow the provided steps to integrate the services into your project.
We use the Relay service to obfuscate the Hosts' IP, while still allowing them to locally host strangers.
With those services set up and your project linked to your cloud organization, open the **mainScene** scene in the Editor and begin using the Game Lobby Sample.
[Relay Overview](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview)
[Relay Dashboard]( https://dashboard.unity3d.com/relay)
### Running the sample
You will need two “players” to demonstrate the full sample functionality. Create a standalone build to run alongside the Editor in Play mode. Although Auth creates anonymous credentials using your machine’s registry, your Editor and your build have different credentials because they create different registry entries.
### Setup
For either one, select "About & Support => Get Started"
**Closed Beta Only**
#### **Lobby Join Menu**
Follow the steps, downloading your packaged folders to the Sample Project Package\Packages
![alt_text](~Documents/1_lobby_list.PNG "Lobby List")
*If you open the project and you get the "Enter Safe Mode" dialogue, it means you are missing your packages.*
The Lobby Join menu contains the lobby list UI, which acts as a hub for players to connect to each other using the public lobby list or lobby code.
*If you still cannot find the package namespaces, ensure the Assets/Scripts/LobbyRelaySample.asmdef is referencing the packages.*
Follow the steps until you hit "Lobby/Relay On"
1. **Public lobby list**: Shows all lobbies not set to private. Lobbies contain developer-defined data which can be set to public and non-public visibility. The Lobby service cleans up any “zombie” rooms so they don’t appear in this list. For this sample, lobby names and player counts are shown, and lobbies in the “in-game” state are not shown. You can select a lobby and then select **Join**.
2. **Refresh icon**: Refreshes the Lobby List. The Lobby service imposes rate limits on all API calls to prevent spamming. Refresh attempts within the rate limit will do nothing (approximately every 1.5 seconds, see [Lobby documentation](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service) for details).
3. **Lobby Code field**: Enter a lobby code for an existing lobby. In addition to the public lobby list, all lobbies can be joined using their codes. This allows players to privately share access to lobbies.
4. **Filters**: Sets the Lobby List to only show servers of a certain color. The Lobby service can filter any queries by data set to public visibility. For this sample, players can optionally filter by color, which hosts set for their lobbies.
5. **Join**:** **Requests to join by public lobby list selection or lobby code. Failed requests are also rate limited to prevent spam, if the player presses the button repeatedly.
6. **Create**: Allows creation of a new lobby. Players select a lobby name and whether to make a private lobby, and they then connect to the new lobby as its host.
7. **Player name**: Displays the player name and allows renaming. By default, players are assigned a name based on their anonymous Auth credentials, but name changes follow their credentials so that all players see the new name.
## Solo Testing
Create a new Unity Build of the project in the OS of your choice.
Because the Authentication service creates a unique ID for builds, you will need to host a lobby in Build and join in Editor or vice versa.
#### **Lobby View**
1. Start the game, and hit start to enter the Room List. This Queries the rooms service for available Lobbies, there wont be any right now.
![alt_text](~Documents/2_lobby.PNG "Lobby")
![Join Menu](~Documentation/Images/tutorial_1_lobbyList.png?raw=true "Join Menu")
2. The Create Menu Lets you make a new Lobby.
The Lobby View UI displays information from Lobby and Relay for all players in a lobby. When a new player joins, they immediately begin connecting to the host, after which they synchronize emotes and state changes with the other lobby members.
![Create Menu](~Documentation/Images/tutorial_2_createMenu.png?raw=true)
3. This is the Lobby, It has a Room code for you to share with your friends to allow them to join.
For demonstration purposes we also show the Relay Code, which will be passed to all users in the Lobby.
![Lobby View](~Documentation/Images/tutorial_3_HostGame.png?raw=true)
1. **Lobby name**: Set when the lobby was created and cannot be changed.
2. **Lobby Code**: Shareable code generated by the Lobby service. This may be provided externally to other players to allow them to join this lobby.
3. **Lobby user**: A player in the lobby. The player’s name, state, and emote are displayed; this data is synchronized through Relay + UTP, so any changes that a player makes will appear immediately for all connected players. Incoming players will be sent the current data once they have connected.
4. **Relay IP**:** **The anonymous server IP that Relay generates. This does not need to be shown to players and is displayed here simply to indicate that Relay is functioning.
5. **Relay Code**: An internal join code generated by Relay that is used during Relay connection. This does not need to be shown to players and is displayed here simply to indicate that Relay is functioning.
6. **Emote icons**: Sets the player’s emote and is synchronized using UTP.
7. **Lobby color**: (Host only) Sets the lobby color for filtering in the Lobby List. This is synchronized through Lobby, so changes won’t appear immediately for all players because Lobby queries are rate limited. See Rate Limits.
8. **Ready button**: Sets a ready state on the player. When all players are ready, the host initiates a countdown to an “in-game” state, and the lobby becomes hidden from the public lobby list.
4. Open the second game instance in Editor or in Build, you should now see your Lobby in the list.
### Architecture
![Populated Join View](~Documentation/Images/tutorial_4_newLobby.png?raw=true)
The Game Lobby Sample is designed as a vertical slice of a multiplayer lobby, so it has additional infrastructure that might be expected in full game production, as well as some components to allow multiple services to work together. As such, not all of the codebase will be relevant depending on your needs. Most contents are self-documenting, but some high-level points follow:
5. The Lobby holds up to 4 players and will pass the Relay code once all the players are ready.
![Relay Ready!](~Documentation/Images/tutorial_5_editorCow.png?raw=true)
* Logic for using the Auth, Lobby, and Relay services are encapsulated in their own directories. Much of the API usage is abstracted away for convenience.
* For example, **LobbyAPIInterface** contains the actual calls into the Lobby API, but **LobbyAsyncRequests** has additional processing on the results of those calls as well as some structures necessary for timing the API calls properly.
* The Relay directory also contains logic for using the Unity Transport (UTP) because it requires a transport to function.
* The **Game** directory contains core “glue” classes for running the sample itself, representing a simple framework for a game.
* **GameManager** has all the core logic for running the sample. It sets up the services and UI, manages game states, and fields messages from other components.
* Various other classes exist here to maintain the states of multiple components of the sample and to interface between our sample’s needs for Lobby and Relay data and the structure of that data remotely in the services.
* The **Infrastructure** directory contains classes used for essential tasks related to overall function but not specifically to any service.
* **Locator **mimics a Service Locator pattern, allowing for behaviors that might otherwise be Singletons to easily be swapped in and out.
* **Messenger **creates a simple messaging system used to keep unrelated classes decoupled, letting them instead message arbitrary listeners when interesting things happen. For example, a lobby is updated, or a player disconnects from a relay.
* An** Observer** pattern is used for all UI elements and for local copies of remote Lobby and Relay data. An Observer is alerted whenever its observed data changes, and the owner of that data doesn’t need to know who is observing.
* The **UI **directory strictly contains logic for the sample’s UI and observing relevant data. Viewing these files should not be necessary to understand how to use the services themselves, though they do demonstrate the use of the Observer pattern.
* Several files exist with classes that simply implement **ObserverBehaviour**. This is because Unity requires **MonoBehaviours** to exist in files of the same names.
* Note that much of the UI is driven by **CanvasGroup** alpha for visibility, which means that some behaviors continue to run even when invisible to the player.
* Multiple **Tests** directories are included to demonstrate core behavior and edge cases for some of the code. In particular, the Play mode tests for Lobby and Relay can be used to ensure your connection to the services is functioning correctly.
* In the Editor, the project assets are broken into nested prefabs for convenience when making changes during sample development. Their details should not be considered vital, although there are UI elements that depend on event handlers that are serialized.
6. The countdown will start after the rooms data synch has completed. (It is a little slow due to our refresh rate being low at the moment)
### Considerations
![Countdown!](~Documentation/Images/tutorial_6_countDown.png?raw=true)
While the Game Lobby Sample represents more than just a minimal implementation of the Lobby and Relay services, it is not comprehensive and some design decisions were made for faster or more readable development.
7. The relay service IP gets passed to all users in the lobby, and this is where you would connect to a server, if you had one.
![InGame!](~Documentation/Images/tutorial_7_ingame.png?raw=true)
* All operations using Lobby and Relay rely on asynchronous API calls. The sample code has some logic for handling issues that can result from receiving the results at arbitrary times, but it doesn’t have logic for enqueuing calls that the user initiates during setup and cleanup. Rapid operations when entering and exiting lobbies can result in unexpected behavior.
* Relay does not support host migration, but Lobby does. If a lobby host disconnects, the lobby might seem to clients to continue to operate until Lobby detects the disconnect. In practice, you might want to implement an additional periodic handshake between hosts and clients in cases where data could get out of sync quickly.
* The sample sets up heartbeat pings with Lobby and Relay to keep the connections alive, but they do not impose any limitations on a lobby’s duration. Consider a maximum duration in actual use, such as a maximum game length.
* HTTP errors will appear in the console. These are returned by Lobby and Relay API calls for various reasons. In general, they do not impact the sample’s execution, though they might result in unexpected behavior for a player since the sample doesn’t provide any explanatory UI when these errors occur.
* **List of HTTP errors:**
* **404 (“Not Found”) errors** might occur when the Lobby service handles multiple incoming API calls in an arbitrary order, usually when leaving a lobby. They will also occur if trying to join an invalid lobby, such as one that has been deleted but still appears in the lobby list before refreshing.
* **429 (“Too Many Requests”) errors** occur if rate limited operations happen too quickly. In particular, refreshing the lobby list too quickly results in 429 errors from the **QueryLobbiesAsync** call. Consult the Lobby documentation for details.
* **401 (“Unauthorized”) errors** occur if the user enters the lobby menu before Auth sign-in completes, since all Lobby and Relay operations require Auth credentials.
* **409 (“Conflict”) errors** occur if a player tries to join a lobby using the same credentials as another player. In particular, this will happen if you are trying to test with multiple standalone builds, since they share the same registry entry on your machine. To test with three or more players on one machine:
* Create a duplicate project with Symbolic Links to the original **Assets **and **Packages**, so that it uses the same assets. Copy the **ProjectSettings** as well, but do not link them to the original. Note that the process for creating Symbolic Links will depend on your operating system.
* Open this project in a second Editor.
* Under **Edit **>** Project Settings **>** Player**, modify the **Product Name**. This causes the duplicate project to have a new registry entry, so Auth will assign new credentials.
* Verify that running the sample in either Play mode or a standalone build assigns a different default player name than the original. This indicates different Auth credentials, preventing the 409 errors.

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


fileFormatVersion: 2
guid: 48ec34a3875818e4690f1bf0be69ccd9
guid: 771ddda4ce2ee2a4dad57866ecc1170d
MonoImporter:
externalObjects: {}
serializedVersion: 2

8
Assets/Art/Crown.meta


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

1001
Assets/Prefabs/UI/PopUpUI.prefab
文件差异内容过多而无法显示
查看文件

7
Assets/Prefabs/UI/PopUpUI.prefab.meta


fileFormatVersion: 2
guid: 79d6084439b78bb4eaf5232cb953fd87
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

44
Assets/Scripts/Infrastructure/AsyncRequest.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LobbyRelaySample
{
/// <summary>
/// Both Lobby and Relay have need for asynchronous requests with some basic safety wrappers. This is a shared place for that.
/// </summary>
public static class AsyncRequest
{
public static async void DoRequest(Task task, Action onComplete)
{
string currentTrace = System.Environment.StackTrace; // For debugging. If we don't get the calling context here, it's lost once the async operation begins.
try
{ await task;
}
catch (Exception e)
{ Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
}
finally
{ onComplete?.Invoke();
}
}
public static async void DoRequest<T>(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace;
try
{ result = await task;
}
catch (Exception e)
{ Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
}
finally
{ onComplete?.Invoke(result);
}
}
}
}

11
Assets/Scripts/Infrastructure/AsyncRequest.cs.meta


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

32
Assets/Scripts/Infrastructure/LogHandlerSettings.cs


using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace LobbyRelaySample
{
public class LogHandlerSettings : MonoBehaviour
{
[SerializeField]
[Tooltip("Only logs of this level or higher will appear in the console.")]
private LogMode m_editorLogVerbosity = LogMode.Critical;
[SerializeField]
private PopUpUI m_popUpPrefab;
[SerializeField]
private ErrorReaction m_errorReaction;
void Awake()
{
LogHandler.Get().mode = m_editorLogVerbosity;
LogHandler.Get().SetLogReactions(m_errorReaction);
}
public void SpawnErrorPopup(string errorMessage)
{
var popupInstance = Instantiate(m_popUpPrefab, transform);
popupInstance.ShowPopup(errorMessage, Color.red);
}
}
}

11
Assets/Scripts/Infrastructure/LogHandlerSettings.cs.meta


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

66
Assets/Scripts/Relay/RelayAPIInterface.cs


using System;
using Unity.Services.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
namespace LobbyRelaySample.relay
{
/// <summary>
/// Wrapper for all the interaction with the Relay API.
/// </summary>
public static class RelayAPIInterface
{
/// <summary>
/// A Relay Allocation represents a "server" for a new host.
/// </summary>
public static async void AllocateAsync(int maxConnections, Action<Allocation> onComplete)
{
try
{
Allocation allocation = await Relay.Instance.CreateAllocationAsync(maxConnections);
onComplete.Invoke(allocation);
}
catch (RelayServiceException ex)
{
Debug.LogError($"Relay AllocateAsync returned a relay exception: {ex.Reason} - {ex.Message}");
throw;
}
}
/// <summary>
/// Only after an Allocation has been completed can a Relay join code be obtained. This code will be stored in the lobby's data as non-public
/// such that players can retrieve the Relay join code only after connecting to the lobby.
/// </summary>
public static async void GetJoinCodeAsync(Guid hostAllocationId, Action<string> onComplete)
{
try
{
string joinCode = await Relay.Instance.GetJoinCodeAsync(hostAllocationId);
onComplete.Invoke(joinCode);
}
catch (RelayServiceException ex)
{
Debug.LogError($"Relay GetJoinCodeAsync returned a relay exception: {ex.Reason} - {ex.Message}");
throw;
}
}
/// <summary>
/// Clients call this to retrieve the host's Allocation via a Relay join code.
/// </summary>
public static async void JoinAsync(string joinCode, Action<JoinAllocation> onComplete)
{
try
{
JoinAllocation joinAllocation = await Relay.Instance.JoinAllocationAsync(joinCode);
onComplete.Invoke(joinAllocation);
}
catch (RelayServiceException ex)
{
Debug.LogError($"Relay JoinCodeAsync returned a relay exception: {ex.Reason} - {ex.Message}");
throw;
}
}
}
}

243
Assets/Scripts/Relay/RelayUtpClient.cs


using System.Collections.Generic;
using Unity.Networking.Transport;
using UnityEngine;
using MsgType = LobbyRelaySample.relay.RelayUtpSetup.MsgType;
namespace LobbyRelaySample.relay
{
/// <summary>
/// This observes the local player and updates remote players over Relay when there are local changes, demonstrating basic data transfer over the Unity Transport (UTP).
/// Created after the connection to Relay has been confirmed.
/// </summary>
public class RelayUtpClient : MonoBehaviour // This is a MonoBehaviour merely to have access to Update.
{
protected LobbyUser 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.
protected bool m_hasSentInitialMessage = false;
private const float k_heartbeatPeriod = 5;
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby)
{
m_localUser = localUser;
m_localLobby = localLobby;
m_localUser.onChanged += OnLocalChange;
m_networkDriver = networkDriver;
m_connections = connections;
Locator.Get.UpdateSlow.Subscribe(UpdateSlow, k_heartbeatPeriod);
}
protected virtual void Uninitialize()
{
m_localUser.onChanged -= OnLocalChange;
Leave();
Locator.Get.UpdateSlow.Unsubscribe(UpdateSlow);
m_networkDriver.Dispose();
}
public void OnDestroy()
{
Uninitialize();
}
private void OnLocalChange(LobbyUser localUser)
{
if (m_connections.Count == 0) // This could be the case for the host alone in the lobby.
return;
foreach (NetworkConnection conn in m_connections)
DoUserUpdate(m_networkDriver, conn, m_localUser);
}
private void Update()
{
OnUpdate();
}
private void UpdateSlow(float dt)
{
// Clients need to send any data over UTP periodically, or else the connection will timeout.
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, "0", MsgType.Ping, 0); // The ID doesn't matter here, so send a minimal number of bytes.
}
protected virtual void OnUpdate()
{
if (!m_hasSentInitialMessage)
ReceiveNetworkEvents(m_networkDriver); // Just on the first execution, make sure to handle any events that accumulated while completing the connection.
m_networkDriver.ScheduleUpdate().Complete(); // This pumps all messages, which pings the Relay allocation and keeps it alive. It should be called no more often than ReceiveNetworkEvents.
ReceiveNetworkEvents(m_networkDriver); // This reads the message queue which was just updated.
if (!m_hasSentInitialMessage)
SendInitialMessage(m_networkDriver, m_connections[0]); // On a client, the 0th (and only) connection is to the host.
}
private void ReceiveNetworkEvents(NetworkDriver driver)
{
NetworkConnection conn;
DataStreamReader strm;
NetworkEvent.Type cmd;
while ((cmd = driver.PopEvent(out conn, out strm)) != NetworkEvent.Type.Empty) // NetworkConnection also has PopEvent, but NetworkDriver.PopEvent automatically includes new connections.
{
ProcessNetworkEvent(conn, strm, cmd);
}
}
// See the Write* methods for the expected event format.
private void ProcessNetworkEvent(NetworkConnection conn, DataStreamReader strm, NetworkEvent.Type cmd)
{
if (cmd == NetworkEvent.Type.Data)
{
MsgType msgType = (MsgType)strm.ReadByte();
string id = ReadLengthAndString(ref strm);
if (id == m_localUser.ID || !m_localLobby.LobbyUsers.ContainsKey(id)) // We don't hold onto messages, since an incoming user will be fully initialized before they send events.
return;
if (msgType == MsgType.PlayerName)
{
string name = ReadLengthAndString(ref strm);
m_localLobby.LobbyUsers[id].DisplayName = name;
}
else if (msgType == MsgType.Emote)
{
EmoteType emote = (EmoteType)strm.ReadByte();
m_localLobby.LobbyUsers[id].Emote = emote;
}
else if (msgType == MsgType.ReadyState)
{
UserStatus status = (UserStatus)strm.ReadByte();
m_localLobby.LobbyUsers[id].UserStatus = status;
}
else if (msgType == MsgType.StartCountdown)
Locator.Get.Messenger.OnReceiveMessage(MessageType.StartCountdown, null);
else if (msgType == MsgType.CancelCountdown)
Locator.Get.Messenger.OnReceiveMessage(MessageType.CancelCountdown, null);
else if (msgType == MsgType.ConfirmInGame)
Locator.Get.Messenger.OnReceiveMessage(MessageType.ConfirmInGameState, null);
else if (msgType == MsgType.EndInGame)
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
ProcessNetworkEventDataAdditional(conn, strm, msgType, id);
}
else if (cmd == NetworkEvent.Type.Disconnect)
ProcessDisconnectEvent(conn, strm);
}
protected virtual void ProcessNetworkEventDataAdditional(NetworkConnection conn, DataStreamReader strm, MsgType msgType, string id) { }
protected virtual void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)
{
// The host disconnected, and Relay does not support host migration. So, all clients should disconnect.
Debug.LogError("Host disconnected! Leaving the lobby.");
Leave();
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeGameState, GameState.JoinMenu);
}
/// <summary>
/// Relay uses raw pointers for efficiency. This converts them to byte arrays, assuming the stream contents are 1 byte for array length followed by contents.
/// </summary>
unsafe private string ReadLengthAndString(ref DataStreamReader strm)
{
byte length = strm.ReadByte();
byte[] bytes = new byte[length];
fixed (byte* ptr = bytes)
{
strm.ReadBytes(ptr, length);
}
return System.Text.Encoding.UTF8.GetString(bytes);
}
/// <summary>
/// Once a client is connected, send a message out alerting the host.
/// </summary>
private void SendInitialMessage(NetworkDriver driver, NetworkConnection connection)
{
WriteByte(driver, connection, m_localUser.ID, MsgType.NewPlayer, 0);
ForceFullUserUpdate(driver, connection, m_localUser); // Assuming this is only created after the Relay connection is successful.
m_hasSentInitialMessage = true;
}
/// <summary>
/// When player data is updated, send out events for just the data that actually changed.
/// </summary>
private void DoUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser 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);
}
/// <summary>
/// Sometimes (e.g. when a new player joins), we need to send out the full current state of this player.
/// </summary>
protected void ForceFullUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user)
{
// Note that it would be better to send a single message with the full state, but for the sake of shorter code we'll leave that out here.
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);
}
/// <summary>
/// Write string data as: [1 byte: msgType] [1 byte: id length N] [N bytes: id] [1 byte: string length M] [M bytes: string]
/// </summary>
protected void WriteString(NetworkDriver driver, NetworkConnection connection, string id, MsgType msgType, string str)
{
byte[] idBytes = System.Text.Encoding.UTF8.GetBytes(id);
byte[] strBytes = System.Text.Encoding.UTF8.GetBytes(str);
List<byte> message = new List<byte>(idBytes.Length + strBytes.Length + 3);
message.Add((byte)msgType);
message.Add((byte)idBytes.Length);
message.AddRange(idBytes);
message.Add((byte)strBytes.Length);
message.AddRange(strBytes);
if (driver.BeginSend(connection, out var dataStream) == 0) // Oh, should check this first?
{
byte[] bytes = message.ToArray();
unsafe
{
fixed (byte* bytesPtr = bytes)
{
dataStream.WriteBytes(bytesPtr, message.Count);
driver.EndSend(dataStream);
}
}
}
}
/// <summary>
/// Write byte data as: [1 byte: msgType] [1 byte: id length N] [N bytes: id] [1 byte: data]
/// </summary>
protected void WriteByte(NetworkDriver driver, NetworkConnection connection, string id, MsgType msgType, byte value)
{
byte[] idBytes = System.Text.Encoding.UTF8.GetBytes(id);
List<byte> message = new List<byte>(idBytes.Length + 3);
message.Add((byte)msgType);
message.Add((byte)idBytes.Length);
message.AddRange(idBytes);
message.Add(value);
if (driver.BeginSend(connection, out var dataStream) == 0) // Oh, should check this first?
{
byte[] bytes = message.ToArray();
unsafe
{
fixed (byte* bytesPtr = bytes)
{
dataStream.WriteBytes(bytesPtr, message.Count);
driver.EndSend(dataStream);
}
}
}
}
public void Leave()
{
foreach (NetworkConnection connection in m_connections)
connection.Disconnect(m_networkDriver);
m_localLobby.RelayServer = null;
}
}
}

11
Assets/Scripts/Relay/RelayUtpClient.cs.meta


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

146
Assets/Scripts/Relay/RelayUtpHost.cs


using System.Collections.Generic;
using Unity.Networking.Transport;
using MsgType = LobbyRelaySample.relay.RelayUtpSetup.MsgType;
namespace LobbyRelaySample.relay
{
/// <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.
/// </summary>
public class RelayUtpHost : RelayUtpClient, IReceiveMessages
{
public override void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby)
{
base.Initialize(networkDriver, connections, localUser, localLobby);
m_hasSentInitialMessage = true; // The host will be alone in the lobby at first, so they need not send any messages right away.
Locator.Get.Messenger.Subscribe(this);
}
protected override void Uninitialize()
{
base.Uninitialize();
Locator.Get.Messenger.Unsubscribe(this);
}
protected override void OnUpdate()
{
base.OnUpdate();
DoHeartbeat();
}
/// <summary>
/// When a new client connects, they need to be updated with the current state of everyone else.
/// </summary>
private void OnNewConnection(NetworkConnection conn)
{
foreach (var user in m_localLobby.LobbyUsers) // The host includes itself here since we don't necessarily have an ID available, but it will ignore its own messages on arrival.
ForceFullUserUpdate(m_networkDriver, conn, user.Value);
}
protected override void ProcessNetworkEventDataAdditional(NetworkConnection conn, DataStreamReader strm, MsgType msgType, string id)
{
// Note that the strm contents might have already been consumed, depending on the msgType.
// Forward messages from clients to other clients.
if (msgType == MsgType.PlayerName)
{
string name = m_localLobby.LobbyUsers[id].DisplayName;
foreach (NetworkConnection otherConn in m_connections)
{
if (otherConn == conn)
continue;
WriteString(m_networkDriver, otherConn, id, msgType, name);
}
}
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;
foreach (NetworkConnection otherConn in m_connections)
{
if (otherConn == conn)
continue;
WriteByte(m_networkDriver, otherConn, id, msgType, value);
}
}
else if (msgType == MsgType.NewPlayer) // This ensures clients in builds are sent player state once they establish that they can send (and receive) events.
OnNewConnection(conn);
// If a client has changed state, check if this changes whether all players have readied.
if (msgType == MsgType.ReadyState)
CheckIfAllUsersReady();
}
protected override void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)
{
// When a disconnect from the host occurs, no additional action is required. This override just prevents the base behavior from occurring.
// TODO: If a client disconnects, see if remaining players are all already ready.
UnityEngine.Debug.LogError("Client disconnected!");
}
public void OnReceiveMessage(MessageType type, object msg)
{
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.
{
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.EndInGame, 0);
}
}
private void CheckIfAllUsersReady()
{
bool haveAllReadied = true;
foreach (var user in m_localLobby.LobbyUsers)
{
if (user.Value.UserStatus != UserStatus.Ready)
{ haveAllReadied = false;
break;
}
}
if (haveAllReadied && m_localLobby.State == LobbyState.Lobby) // Need to notify both this client and all others that all players have readied.
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.StartCountdown, null);
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.StartCountdown, 0);
}
else if (!haveAllReadied && m_localLobby.State == LobbyState.CountDown) // Someone cancelled during the countdown, so abort the countdown.
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.CancelCountdown, null);
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.CancelCountdown, 0);
}
}
/// <summary>
/// In an actual game, after the countdown, there would be some step here where the host and all clients sync up on game state, load assets, etc.
/// Here, we will instead just signal an "in-game" state that can be ended by the host.
/// </summary>
public void SendInGameState()
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.ConfirmInGameState, null);
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.ConfirmInGame, 0);
}
/// <summary>
/// Clean out destroyed connections, and accept all new ones.
/// </summary>
private void DoHeartbeat()
{
for (int c = m_connections.Count - 1; c >= 0; c--)
{
if (!m_connections[c].IsCreated)
m_connections.RemoveAt(c);
}
while (true)
{
var conn = m_networkDriver.Accept(); // Note that since we pumped the event queue earlier in Update, m_networkDriver has been updated already this frame.
if (!conn.IsCreated) // "Nothing more to accept" is signalled by returning an invalid connection from Accept.
break;
m_connections.Add(conn);
OnNewConnection(conn); // This ensures that clients in editors are sent player state once they establish a connection. The timing differs slightly from builds.
}
}
}
}

11
Assets/Scripts/Relay/RelayUtpHost.cs.meta


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

225
Assets/Scripts/Relay/RelayUtpSetup.cs


using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Networking.Transport;
using Unity.Networking.Transport.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
namespace LobbyRelaySample.relay
{
/// <summary>
/// Responsible for setting up a connection with Relay using UTP, for the lobby host.
/// 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;
public enum MsgType { Ping = 0, NewPlayer, ReadyState, PlayerName, Emote, StartCountdown, CancelCountdown, ConfirmInGame, EndInGame }
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();
/// <summary>
/// Shared behavior for binding to the Relay allocation, which is required for use.
/// Note that a host will send bytes from the Allocation it creates, whereas a client will send bytes from the JoinAllocation it receives using a relay code.
/// </summary>
protected void BindToAllocation(string ip, int port, byte[] allocationIdBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes, byte[] hmacKeyBytes, int connectionCapacity)
{
NetworkEndPoint serverEndpoint = NetworkEndPoint.Parse(ip, (ushort)port);
RelayAllocationId allocationId = ConvertAllocationIdBytes(allocationIdBytes);
RelayConnectionData connectionData = ConvertConnectionDataBytes(connectionDataBytes);
RelayConnectionData hostConnectionData = ConvertConnectionDataBytes(hostConnectionDataBytes);
RelayHMACKey key = ConvertHMACKeyBytes(hmacKeyBytes);
m_endpointForServer = serverEndpoint;
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref hostConnectionData, ref key);
relayServerData.ComputeNewNonce();
var relayNetworkParameter = new RelayNetworkParameter { ServerData = relayServerData };
m_networkDriver = NetworkDriver.Create(new INetworkParameter[] { relayNetworkParameter });
m_connections = new List<NetworkConnection>(connectionCapacity);
if (m_networkDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
Debug.LogError("Failed to bind to Relay allocation.");
else
StartCoroutine(WaitForBindComplete());
}
private IEnumerator WaitForBindComplete()
{
while (!m_networkDriver.Bound)
{
m_networkDriver.ScheduleUpdate().Complete();
yield return null;
}
OnBindingComplete();
}
protected abstract void OnBindingComplete();
#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);
}
}
unsafe private static RelayConnectionData ConvertConnectionDataBytes(byte[] connectionData)
{
fixed (byte* ptr = connectionData)
{
return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length);
}
}
unsafe private static RelayHMACKey ConvertHMACKeyBytes(byte[] hmac)
{
fixed (byte* ptr = hmac)
{
return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length);
}
}
#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, OnAllocation);
}
private void OnAllocation(Allocation allocation)
{
m_allocation = allocation;
RelayAPIInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode);
BindToAllocation(allocation.RelayServer.IpV4, allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.ConnectionData, allocation.ConnectionData, allocation.Key, 16);
}
private void OnRelayCode(string relayCode)
{
m_localLobby.RelayCode = relayCode;
m_localLobby.RelayServer = new ServerAddress(m_allocation.RelayServer.IpV4, m_allocation.RelayServer.Port);
m_joinState |= JoinState.Joined;
CheckForComplete();
}
protected override void OnBindingComplete()
{
if (m_networkDriver.Listen() != 0)
{
Debug.LogError("Server failed to listen");
m_onJoinComplete(false, null);
}
else
{
Debug.LogWarning("Server is now listening!");
m_joinState |= JoinState.Bound;
CheckForComplete();
}
}
private void CheckForComplete()
{
if (m_joinState == (JoinState.Joined | JoinState.Bound))
{
m_isRelayConnected = true;
RelayUtpHost host = gameObject.AddComponent<RelayUtpHost>();
host.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, host);
LobbyAsyncRequests.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode, null);
}
}
}
/// <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;
}
private void OnLobbyChange(LocalLobby lobby)
{
if (m_localLobby.RelayCode != null)
{
RelayAPIInterface.JoinAsync(m_localLobby.RelayCode, OnJoin);
m_localLobby.onChanged -= OnLobbyChange;
}
}
private void OnJoin(JoinAllocation joinAllocation)
{
if (joinAllocation == null)
return;
m_allocation = joinAllocation;
BindToAllocation(joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, joinAllocation.AllocationIdBytes, joinAllocation.ConnectionData, joinAllocation.HostConnectionData, joinAllocation.Key, 1);
m_localLobby.RelayServer = new ServerAddress(joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port);
}
protected override void OnBindingComplete()
{
StartCoroutine(ConnectToServer());
}
private IEnumerator 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();
yield return null;
}
if (m_networkDriver.GetConnectionState(m_connections[0]) != NetworkConnection.State.Connected)
{
Debug.LogError("Client failed to connect to server");
m_onJoinComplete(false, null);
}
else
{
m_isRelayConnected = true;
RelayUtpClient client = gameObject.AddComponent<RelayUtpClient>();
client.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, client);
LobbyAsyncRequests.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode, null);
}
}
}
}

11
Assets/Scripts/Relay/RelayUtpSetup.cs.meta


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

25
Assets/Scripts/UI/PopUpUI.cs


using LobbyRelaySample.UI;
using TMPro;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
namespace LobbyRelaySample
{
public class PopUpUI : MonoBehaviour
{
[SerializeField]
TMP_InputField m_popupText;
public void ShowPopup(string newText, Color textColor = default)
{
m_popupText.SetTextWithoutNotify(newText);
m_popupText.textComponent.color = textColor;
}
public void Delete()
{
Destroy(gameObject);
}
}
}

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


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

35
Assets/Scripts/UI/RateLimitVisibility.cs


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

39
Assets/Scripts/UI/RecolorForLobbyType.cs


using UnityEngine;
using UnityEngine.UI;
namespace LobbyRelaySample.UI
{
/// <summary>
/// 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.75f, 0.5f, 0.1f);
private static readonly Color s_greenColor = new Color(0.5f, 1, 0.7f);
private static readonly Color s_blueColor = new Color(0.75f, 0.7f, 1);
private static readonly Color[] s_colorsOrdered = new Color[] { Color.white, s_orangeColor, s_greenColor, s_blueColor };
[SerializeField]
private Graphic[] m_toRecolor;
private LocalLobby m_lobby;
public void UpdateLobby(LocalLobby lobby)
{
m_lobby = lobby;
Color color = s_colorsOrdered[(int)lobby.Color];
foreach (Graphic graphic in m_toRecolor)
graphic.color = new Color(color.r, color.g, color.b, graphic.color.a);
}
/// <summary>
/// Called in-editor by toggles to set the color of the lobby.
/// </summary>
public void ChangeColor(int color)
{
if (m_lobby != null)
m_lobby.Color = (LobbyColor)color;
}
}
}

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


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

17
Assets/Scripts/Game/EmoteType.cs


namespace LobbyRelaySample
{
public enum EmoteType { None = 0, Smile, Frown, Shock, Laugh }
public static class EmoteTypeExtensions
{
public static string GetString(this EmoteType emote)
{
return
emote == EmoteType.Smile ? ":D" :
emote == EmoteType.Frown ? ":(" :
emote == EmoteType.Shock ? ":O" :
emote == EmoteType.Laugh ? "XD" :
"";
}
}
}

11
Assets/Scripts/Game/EmoteType.cs.meta


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

331
Assets/Scripts/Game/GameManager.cs


using LobbyRelaySample.relay;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// Sets up and runs the entire sample.
/// </summary>
public class GameManager : MonoBehaviour, IReceiveMessages
{
/// <summary>
/// All these should be assigned the observers in the scene at the start.
/// </summary>
#region UI elements that observe the local state. These are
[SerializeField]
private List<LocalGameStateObserver> m_GameStateObservers = new List<LocalGameStateObserver>();
[SerializeField]
private List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>();
[SerializeField]
private List<LobbyUserObserver> m_LocalUserObservers = new List<LobbyUserObserver>();
[SerializeField]
private List<LobbyServiceDataObserver> m_LobbyServiceObservers = new List<LobbyServiceDataObserver>();
#endregion
private LocalGameState m_localGameState = new LocalGameState();
private LobbyUser m_localUser;
private LocalLobby m_localLobby;
private LobbyServiceData m_lobbyServiceData = new LobbyServiceData();
private LobbyContentHeartbeat m_lobbyContentHeartbeat = new LobbyContentHeartbeat();
private RelayUtpSetup m_relaySetup;
private RelayUtpClient m_relayClient;
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
public void SetLobbyColorFilter(int color)
{
m_lobbyColorFilter = (LobbyColor)color;
}
private LobbyColor m_lobbyColorFilter;
#region Setup
private 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
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn));
Application.wantsToQuit += OnWantToQuit;
}
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();
}
private void OnAuthSignIn()
{
Debug.Log("Signed in.");
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.
}
private void BeginObservers()
{
foreach (var gameStateObs in m_GameStateObservers)
gameStateObs.BeginObserving(m_localGameState);
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);
}
#endregion
/// <summary>
/// Primarily used for UI elements to communicate state changes, this will receive messages from arbitrary providers for user interactions.
/// </summary>
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.RenameRequest)
{ m_localUser.DisplayName = (string)msg;
}
else if (type == MessageType.CreateLobbyRequest)
{
var createLobbyData = (LocalLobby)msg;
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnCreatedLobby();
},
OnFailedJoin);
}
else if (type == MessageType.JoinLobbyRequest)
{
LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg;
LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, m_localUser, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnJoinedLobby();
},
OnFailedJoin);
}
else if (type == MessageType.QueryLobbies)
{
m_lobbyServiceData.State = LobbyQueryState.Fetching;
LobbyAsyncRequests.Instance.RetrieveLobbyListAsync(
qr => {
if (qr != null)
OnLobbiesQueried(lobby.ToLocalLobby.Convert(qr));
},
er => {
OnLobbyQueryFailed();
},
m_lobbyColorFilter);
}
else if (type == MessageType.ChangeGameState)
{ SetGameState((GameState)msg);
}
else if (type == MessageType.UserSetEmote)
{ EmoteType emote = (EmoteType)msg;
m_localUser.Emote = emote;
}
else if (type == MessageType.LobbyUserStatus)
{ m_localUser.UserStatus = (UserStatus)msg;
}
else if (type == MessageType.StartCountdown)
{ BeginCountDown();
}
else if (type == MessageType.CancelCountdown)
{ m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
}
else if (type == MessageType.ConfirmInGameState)
{ m_localUser.UserStatus = UserStatus.InGame;
m_localLobby.State = LobbyState.InGame;
}
else if (type == MessageType.EndGame)
{ m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
SetUserLobbyState();
}
}
private void SetGameState(GameState state)
{
bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) && m_localGameState.State == GameState.Lobby;
m_localGameState.State = state;
if (isLeavingLobby)
OnLeftLobby();
}
private void OnLobbiesQueried(IEnumerable<LocalLobby> lobbies)
{
var newLobbyDict = new Dictionary<string, LocalLobby>();
foreach (var lobby in lobbies)
newLobbyDict.Add(lobby.LobbyID, lobby);
m_lobbyServiceData.State = LobbyQueryState.Fetched;
m_lobbyServiceData.CurrentLobbies = newLobbyDict;
}
private void OnLobbyQueryFailed()
{
m_lobbyServiceData.State = LobbyQueryState.Error;
}
private void OnCreatedLobby()
{
m_localUser.IsHost = true;
OnJoinedLobby();
}
private void OnJoinedLobby()
{
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
SetUserLobbyState();
StartRelayConnection();
}
private void OnLeftLobby()
{
m_localUser.ResetState();
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID, ResetLocalLobby);
m_lobbyContentHeartbeat.EndTracking();
LobbyAsyncRequests.Instance.EndTracking();
if (m_relaySetup != null)
{
Component.Destroy(m_relaySetup);
m_relaySetup = null;
}
if (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 StartRelayConnection()
{
if (m_localUser.IsHost)
m_relaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
else
m_relaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting);
m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected);
}
private void OnRelayConnected(bool didSucceed, RelayUtpClient client)
{
Component.Destroy(m_relaySetup);
m_relaySetup = null;
if (!didSucceed)
{
Debug.LogError("Relay connection failed! Retrying in 5s...");
StartCoroutine(RetryRelayConnection());
return;
}
m_relayClient = client;
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
private IEnumerator RetryRelayConnection()
{
yield return new WaitForSeconds(5);
StartRelayConnection();
}
private void BeginCountDown()
{
if (m_localLobby.State == LobbyState.CountDown)
return;
m_localLobby.CountDownTime = 4;
m_localLobby.State = LobbyState.CountDown;
StartCoroutine(CountDown());
}
/// <summary>
/// The CountdownUI will pick up on changes to the lobby's countdown timer. This can be interrupted if the lobby leaves the countdown state (via a CancelCountdown message).
/// </summary>
private IEnumerator CountDown()
{
while (m_localLobby.CountDownTime > 0)
{
yield return null;
if (m_localLobby.State != LobbyState.CountDown)
yield break;
m_localLobby.CountDownTime -= Time.deltaTime;
}
if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
}
private void SetUserLobbyState()
{
SetGameState(GameState.Lobby);
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
private 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.CountDownTime = 0;
m_localLobby.RelayServer = null;
}
#region Teardown
/// <summary>
/// In builds, if we are in a lobby and try to send a Leave request on application quit, it won't go through if we're quitting on the same frame.
/// 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()
{
ForceLeaveAttempt();
yield return null;
Application.Quit();
}
private bool OnWantToQuit()
{
bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID);
StartCoroutine(LeaveBeforeQuit());
return canQuit;
}
private void OnDestroy()
{
ForceLeaveAttempt();
}
private void ForceLeaveAttempt()
{
Locator.Get.Messenger.Unsubscribe(this);
if (!string.IsNullOrEmpty(m_localLobby?.LobbyID))
{
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID, null);
m_localLobby = null;
}
}
#endregion
}
}

16
ProjectSettings/BurstAotSettings_StandaloneWindows.json


{
"MonoBehaviour": {
"Version": 3,
"EnableBurstCompilation": true,
"EnableOptimisations": true,
"EnableSafetyChecks": false,
"EnableDebugInAllBuilds": false,
"UsePlatformSDKLinker": false,
"CpuMinTargetX32": 0,
"CpuMaxTargetX32": 0,
"CpuMinTargetX64": 0,
"CpuMaxTargetX64": 0,
"CpuTargetsX32": 6,
"CpuTargetsX64": 72
}
}

6
ProjectSettings/CommonBurstAotSettings.json


{
"MonoBehaviour": {
"Version": 3,
"DisabledWarnings": ""
}
}

1001
README.pdf
文件差异内容过多而无法显示
查看文件

1001
~Documentation/Images/1_lobby_list.PNG
文件差异内容过多而无法显示
查看文件

1001
~Documentation/Images/2_lobby.PNG
文件差异内容过多而无法显示
查看文件

119
Assets/Art/Crown/Crown.png

之前 之后
宽度: 1500  |  高度: 1500  |  大小: 49 KiB

96
Assets/Art/Crown/Crown.png.meta


fileFormatVersion: 2
guid: 57c53a150c76b1f4c9fe27ee2b9a1d17
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

90
Packages/com.unity.transport/.gitattributes


* text=auto eol=lf
**/HavokNative.framework/HavokNative filter=lfs diff=lfs merge=lfs -text
**/cwebp filter=lfs diff=lfs merge=lfs -text
**/moz-cjpeg filter=lfs diff=lfs merge=lfs -text
**/pngcrush filter=lfs diff=lfs merge=lfs -text
*.3[dS][sS] filter=lfs diff=lfs merge=lfs -text
*.DOC diff=astextplain
*.DOCX diff=astextplain
*.DOT diff=astextplain
*.RTF diff=astextplain
*.[aA] filter=lfs diff=lfs merge=lfs -text
*.[aA][iI][fF][cC] filter=lfs diff=lfs merge=lfs -text
*.[aA][iI][fF][fF]? filter=lfs diff=lfs merge=lfs -text
*.[aA][pP][kK] filter=lfs diff=lfs merge=lfs -text
*.[aA][vV][iI] filter=lfs diff=lfs merge=lfs -text
*.[bB][lL][eE][nN][dD] filter=lfs diff=lfs merge=lfs -text
*.[cC][oO][lL][lL][aA][dD][dD][aA] filter=lfs diff=lfs merge=lfs -text
*.[dD][lL][lL] filter=lfs diff=lfs merge=lfs -text
*.[dD][yY][lL][iI][bB] filter=lfs diff=lfs merge=lfs -text
*.[eE][xX][eE] filter=lfs diff=lfs merge=lfs -text
*.[fF][bB][xX] filter=lfs diff=lfs merge=lfs -text
*.[fF][lL][aA] filter=lfs diff=lfs merge=lfs -text
*.[fF][lL][aA][cC] filter=lfs diff=lfs merge=lfs -text
*.[fF][lL][vV] filter=lfs diff=lfs merge=lfs -text
*.[gG][iI][fF] filter=lfs diff=lfs merge=lfs -text
*.[gG][zZ][iI][pP] filter=lfs diff=lfs merge=lfs -text
*.[iI][pP][aA] filter=lfs diff=lfs merge=lfs -text
*.[jJ][aA][rR] filter=lfs diff=lfs merge=lfs -text
*.[jJ][pP][gG] filter=lfs diff=lfs merge=lfs -text
*.[mM]4[vV] filter=lfs diff=lfs merge=lfs -text
*.[mM][kK][vV] filter=lfs diff=lfs merge=lfs -text
*.[mM][oO][vV] filter=lfs diff=lfs merge=lfs -text
*.[mM][pP]3 filter=lfs diff=lfs merge=lfs -text
*.[mM][pP][2-4]? filter=lfs diff=lfs merge=lfs -text
*.[mM][pP][eE]?[gG] filter=lfs diff=lfs merge=lfs -text
*.[oO][bB][jJ] filter=lfs diff=lfs merge=lfs -text
*.[oO][gG][gG] filter=lfs diff=lfs merge=lfs -text
*.[oO][gG][vV] filter=lfs diff=lfs merge=lfs -text
*.[oO][tT][fF] filter=lfs diff=lfs merge=lfs -text
*.[pP][dD][fF] filter=lfs diff=lfs merge=lfs -text
*.[pP][nN][gG] filter=lfs diff=lfs merge=lfs -text
*.[pP][sS][dD] filter=lfs diff=lfs merge=lfs -text
*.[rR][aA][rR] filter=lfs diff=lfs merge=lfs -text
*.[sS][oO] filter=lfs diff=lfs merge=lfs -text
*.[sS][tT][lL] filter=lfs diff=lfs merge=lfs -text
*.[sS][wW][fF] filter=lfs diff=lfs merge=lfs -text
*.[tT][aA][rR] filter=lfs diff=lfs merge=lfs -text
*.[tT][gG][zZ] filter=lfs diff=lfs merge=lfs -text
*.[tT][tT][fF] filter=lfs diff=lfs merge=lfs -text
*.[wW][aA][vV] filter=lfs diff=lfs merge=lfs -text
*.[wW][eE][bB][mM] filter=lfs diff=lfs merge=lfs -text
*.[zZ][iI][pP] filter=lfs diff=lfs merge=lfs -text
*.aif filter=lfs diff=lfs merge=lfs -text
*.anim filter=lfs diff=lfs merge=lfs -text
*.api eol=lf text
*.asset text
*.bundle filter=lfs diff=lfs merge=lfs -text
*.cginc text
*.compute text
*.compute text
*.cs diff=csharp text
*.dat filter=lfs diff=lfs merge=lfs -text
*.doc diff=astextplain
*.docx diff=astextplain
*.dot diff=astextplain
*.entities filter=lfs diff=lfs merge=lfs -text
*.entityheader filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.gradle text eol=lf
*.mat text
*.md text
*.md5 text
*.meta text
*.pdb filter=lfs diff=lfs merge=lfs -text
*.prefab text
*.psb filter=lfs diff=lfs merge=lfs -text
*.raw filter=lfs diff=lfs merge=lfs -text
*.rtf diff=astextplain
*.shader text
*.tga filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tt eol=crlf text
*.txt text
*.unitypackage filter=lfs diff=lfs merge=lfs -text
*.yaml eol=lf
*.yml eol=lf
*/ProjectSettings/*.asset text
Havok.Vdb.dll filter=lfs diff=lfs merge=lfs -text
HavokNative.dll filter=lfs diff=lfs merge=lfs -text
HavokVisualDebugger.exe filter=lfs diff=lfs merge=lfs -text

3
Packages/com.unity.transport/.gitmodules


[submodule "Tools/recipe-engine"]
path = Tools~/recipe-engine
url = ../recipe-engine.git

250
Packages/com.unity.transport/CHANGELOG.md


# Change log
## [0.9.0] - 2021-05-10
### New features
* Added support for long serialization and delta compression.
* Upgraded collections to 1.0.0
* Added a new network interface for WebSockets, can be used in both native and web builds.
### Changes
* Minimum required Unity version has changed to 2020.3.0f1.
* The transport package can be compiled with the tiny c# profile and for WebGL, but WebGL builds only support IPC - not sockets.
### Fixes
### Upgrade guide
## [0.8.0] - 2021-03-23
### New features
* Added overloads of `PopEvent` and `PopEventForConnection` which return the pipeline used as an out parameter.
### Changes
### Fixes
* Fixed some compatility issues with tiny.
* Fixed a crash when sending messages slightly less than one MTU using the fragmentation pipeline.
* Fixed a bug causing `NetworkDriver.RemoteEndPoint` to return an invalid value when using the default network interface.
### Upgrade guide
## [0.7.0] - 2021-02-05
### New features
* Added `DataStreamWriter.WriteRawbits` and `DataStreamWriter.ReadRawBits` for reading and writing raw bits from a data stream.
### Changes
* Optimized the `NetworkCompressionModel` to find buckets in constant time.
* Changed the error behavior of `DataStreamReader` to be consistent between the editor and players.
### Fixes
* Fixed a crash when receiving a packet with an invalid pipeline identifier.
### Upgrade guide
## [0.6.0] - 2020-11-26
### New features
* An error handling pass has been made and `Error.StatusCode` have been added to indicate more specific errors.
* `Error.DisconnectReason` has been added, so when NetworkDriver.PopEvent returns a `NetworkEvent.Type.Disconnect` the reader returned contains 1 byte of data indicating the reason.
### Changes
* The function signature for NetworkDriver.BeginSend has changed. It now returns an `int` value indicating if the function succeeded or not and the DataStreamWriter now instead is returned as a `out` parameter.
* The function signature for INetworkInterface.Initialize has changed. It now requires you to return an `int` value indicating if the function succeeded or not.
* The function signature for INetworkInterface.CreateInterfaceEndPoint has changed. It now requires you to return an `int` value indicating if the function succeeded or not, and NetworkInterfaceEndPoint is now returned as a `out` parameter.
### Fixes
* Fixed a potential crash when receiving a malformated packet.
* Fixed an issue where the DataStream could sometimes fail writing packet uints before the buffer was full.
### Upgrade guide
* `NetworkDriver.BeginSend` now returns an `int` indicating a `Error.StatusCode`, and the `DataStreamWriter` is passed as an `out` parameter.
## [0.5.0] - 2020-10-01
### New features
### Changes
### Fixes
* Fixed display of ipv6 addresses as strings
### Upgrade guide
## [0.4.1] - 2020-09-10
### New features
* Added `NetworkDriver.GetEventQueueSizeForConnection` which allows you to check how many pending events a connection has.
### Changes
### Fixes
* Fixed a compatibility isue with DOTS Runtime.
### Upgrade guide
## [0.4.0-preview.3] - 2020-08-21
### New features
* Added a new fragmentation pipeline which allows you to send messages larger than one MTU. If the `FragmentationPipelineStage` is part of the pipeline you are trying to send with the `NetworkDriver` will allow a `requiredPayloadSize` larger than one MTU to be specified and split the message into multiple packages.
### Changes
* The methods to read and write strings in the `DataStreamReader`/`DataStreamWriter` have been changed to use `FixedString<N>` instead of `NativeString<N>`. The name of the methods have also changed from `ReadString` to `ReadFixedString64` - and similar changes for write and the packed version of the calls. The calls support `FixedString32`, `FixedString64`, `FixedString128`, `FixedString512` and `FixedString4096`.
* Minimum required Unity version has changed to 2020.1.2.
### Fixes
### Upgrade guide
The data stream methods for reading and writing strings have changed, they now take `FixedString64` instead of `NativeString64` and the names have changed as follows:
* `DataStreamReader.ReadString` -> `DataStreamReader.ReadFixedString64`
* `DataStreamReader.ReadPackedStringDelta` -> `DataStreamReader.ReadPackedFixedString64Delta`
* `DataStreamWriter.WriteString` -> `DataStreamWriter.WriteFixedString64`
* `DataStreamWriter.WritePackedStringDelta` -> `DataStreamWriter.WritePackedFixedString64Delta`
The transport now requires Unity 2020.1.2.
## [0.3.1-preview.4] - 2020-06-05
### New features
### Changes
* Added a new `requiredPayloadSize` parameter to `BeginSend`. The required size cannot be larger than `NetworkParameterConstants.MTU`.
* Added errorcode parameter to a `network_set_nonblocking`, `network_set_send_buffer_size` and `network_set_receive_buffer_size` in `NativeBindings`.
* Additional APIs added to `NativeBindings`: `network_set_blocking`, `network_get_send_buffer_size`, `network_get_receive_buffer_size`, `network_set_receive_timeout`, `network_set_send_timeout`.
* Implemented `NetworkEndPoint.AddressAsString`.
### Fixes
* Fixed an issue in the reliable pipeline which would cause it to not recover if one end did not receive packages for a while.
* Fixed `NetworkInterfaceEndPoint` and `NetworkEndPoint` `GetHashCode` implementation.
* Fixed invalid use of strings when specifying the size of socket buffers in the native bindings.
### Upgrade guide
## [0.3.0-preview.6] - 2020-02-24
### New features
### Changes
* Pipelines are now registered by calling `NetworkPipelineStageCollection.RegisterPipelineStage` before creating a `NetworkDriver`. The built-in pipelines do not require explicit registration. The interface for implementing pipelines has been changed to support this.
* NetworkDriver is no longer a generic type. You pass it an interface when creating the `NetworkDriver`, which means you can switch between backends without modifying all usage of the driver. There is a new `NetworkDriver.Create` which creates a driver with the default `NetworkInterface`. It is also possible to create a `new NetworkDriver` by passing a `NetworkInterface` instance as the first argument.
* `NetworkDriver.Send` is replaced by `BeginSend` and `EndSend`. This allows us to do less data copying when sending messages. The interface for implementing new network interfaces has been changed to support this.
* `DataStreamReader` and `DataStreamWriter` no longer owns any memory. They are just reading/writing the data of a `NativeArray<byte>`.
* `DataStreamWriter` has explicit types for all Write methods.
* `DataStreamReader.Context` has been removed.
* Error handling for `DataStreamWriter` has been improved, on failure it returns false and sets `DataStreamWriter.HasFailedWrites` to true. `DataStreamReader` returns a default value and sets `DataStreamReader.HasFailedReads` to true. `DataStreamReader` will throw an exception instead of returning a default value in the editor.
* IPCManager is no longer public, it is still possible to create a `NetworkDriver` with a `IPCNetworkInterface`.
* Added `NetworkDriver.ScheduleFlushSend` which must be called to guarantee that messages are send before next call to `NetworkDriver.ScheduleUpdate`.
* Added `NetworkDriver.LastUpdateTime` to get the update time the `NetworkDriver` used for the most recent update.
* Removed the IPC address family, use a IPv4 localhost address instead.
### Fixes
* Fixed a memory overflow in the reliability pipeline.
* Made the soaker report locale independent.
### Upgrade guide
Creation and type of `NetworkDriver` has changed, use `NetworkDriver.Create` or pass an instance of a `NetworkInterface` to the `NetworkDriver` constructor.
`NetworkDriver.Send` has been replaced by a pair of `NetworkDriver.BeginSend` and `NetworkDriver.EndSend`. Calling `BeginSend` will return a `DataStreamWriter` to which you write the data. The `DataStreamWriter` is then passed to `EndSend`.
All write calls in `DataStreamWriter` need an explicit type, for example `Write(0)` should be replaced by `WriteInt(0)`.
`DataStreamWriter` no longer shares current position between copies, if you call a method which writes you must pass it by ref for the modifications to apply.
`DataStreamWriter` no longer returns a DeferedWriter, you need to take a copy of the writer at the point you want to make modifications and use the copy to overwrite data later.
`DataStreamWriter` is no longer disposable. If you use the allocating constructor you need to use `Allocator.Temp`, if you pass a `NativeArray<byte>` to the constructor the `NativeArray` owns the memory.
`DataStreamReader.Context` no longer exists, you need to pass the `DataStreamReader` itself by ref if you read in a different function.
The interface for network pipelines has been changed.
The interface for network interfaces has been changed.
## [0.2.3-preview.0] - 2019-12-12
### New features
### Changes
* Added reading and write methods for NativeString64 to DataStream.
### Fixes
### Upgrade guide
## [0.2.2-preview.2] - 2019-12-05
### New features
### Changes
* Added a stress test for parallel sending of data.
* Upgraded collections to 0.3.0.
### Fixes
* Fixed a race condition in IPCNetworkInterface.
* Changed NetworkEventQueue to use UnsafeList to get some type safety.
* Fixed an out-of-bounds access in the reliable sequenced pipeline.
* Fixed spelling and broken links in the documentation.
### Upgrade guide
## [0.2.1-preview.1] - 2019-11-28
### New features
### Changes
### Fixes
* Added missing bindings for Linux and Android.
### Upgrade guide
## [0.2.0-preview.4] - 2019-11-26
### New features
### Changes
* Added support for unquantized floats to `DataStream` class.
* Added `NetworkConfigParameter.maxFrameTimeMS` so you to allow longer frame times when debugging to prevent disconnections due to timeout.
* Allow "1.1.1.1:1234" strings when parsing the IP string in the NetworkEndPoint class, it will use the port part when it's present.
* Reliable pipeline now doesn't require parameters passed in (uses default window size of 32)
* Added Read/Write of ulong to `DataStream`.
* Made it possible to get connection state from the parallel NetworkDriver.
* Added `LengthInBits` to the `DataStreamWriter`.
### Fixes
* Do not push data events to disconnected connections. Fixes an error about resetting the queue with pending messages.
* Made the endian checks in `DataStream` compatible with latest version of burst.
### Upgrade guide
## [0.1.2-preview.1] - 2019-07-17
### New features
* Added a new *Ping-Multiplay* sample based on the *Ping* sample.
* Created to be the main sample for demonstrating Multiplay compatibility and best practices (SQP usage, IP binding, etc.).
* Contains both client and server code. Additional details in readme in `/Assets/Samples/Ping-Multiplay/`.
* **DedicatedServerConfig**: Added arguments for `-fps` and `-timeout`.
* **NetworkEndPoint**: Added a `TryParse()` method which returns false if parsing fails
* Note: The `Parse()` method returns a default IP / Endpoint if parsing fails, but a method that could report failure was needed for the Multiplay sample.
* **CommandLine**:
* Added a `HasArgument()` method which returns true if an argument is present.
* Added a `PrintArgsToLog()` method which is a simple way to print launch args to logs.
* Added a `TryUpdateVariableWithArgValue()` method which updates a ref var only if an arg was found and successfully parsed.
### Changes
* Deleted existing SQP code and added reference to SQP Package (now in staging).
* Removed SQP server usage from basic *Ping* sample.
* Note: The SQP server was only needed for Multiplay compatibility, so the addition of *Ping-Multiplay* allowed us to remove SQP from *Ping*.
### Fixes
* **DedicatedServerConfig**: Vsync is now disabled programmatically if requesting an FPS different from the current screen refresh rate.
### Upgrade guide
## [0.1.1-preview.1] - 2019-06-05
### New features
* Moved MatchMaking to a package and supporting code to a separate folder.
### Fixes
* Fixed an issue with the reliable pipeline not resending when completely idle.
### Upgrade guide
## [0.1.0-preview.1] - 2019-04-16
### New features
* Added network pipelines to enable processing of outgoing and incomming packets. The available pipeline stages are `ReliableSequencedPipelineStage` for reliable UDP messages and `SimulatorPipelineStage` for emulating network conditions such as high latency and packet loss. See [the pipeline documentation](com.unity.transport/Documentation~/pipelines-usage.md) for more information.
* Added reading and writing of packed signed and unsigned integers to `DataStream`. These new methods use huffman encoding to reduce the size of transfered data for small numbers.
### Changes
* Enable Burst compilation for most jobs.
* Made it possible to get the remote endpoint for a connection.
* Replacing EndPoint parsing with custom code to avoid having a dependency on `System.Net`.
* Change the ping sample command-line parameters for server to `-port` and `-query_port`.
* For matchmaking, use an Assignment object containing the `ConnectionString`, the `Roster`, and an `AssignmentError` string instead of just the `ConnectionString`.
### Fixes
* Fixed an issue with building iOS on Windows.
* Fixed inconsistent error handling between platforms when the network buffer is full.
### Upgrade guide
Unity 2019.1 is now required.
`BasicNetworkDriver` has been renamed to `GenericNetworkDriver` and a new `UdpNetworkDriver` helper class is also available.
`System.Net` EndPoints can no longer be used as addresses, use the new NetworkEndpoint struct instead.

7
Packages/com.unity.transport/CHANGELOG.md.meta


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

20
Packages/com.unity.transport/DESIGN.md


# Unity Transport Design Rules
## All features are optional
Unity transport is conceptually a thin layer on UDP adding a connection concept. All additional features on top of UDP + connection are optional, when not used they have zero performance or complexity overhead. If possible features are implemented as pipeline stages.
Features that have a limited audience are implemented outside the package - either in game code or other packages.
## Full control over processing time and when packets are sent/received
UTP is optimized for making games. It can be used without creating any additional threads - only using the JobSystem. The layer on top has full control over when the transport schedules jobs. The layer on top also has full control over when packets are sent on the wire. There are no internal buffers delaying messages (except possibly in pipelines).
There is generally no need to continuously poll for messages since incoming data needs to be read right before simulation starts, and we cannot start using new data in the middle of the simulation
## Written in HPC#
All code is jobified and burst compiled, there is no garbage collection. The transport does not spend any processing time outside setup on the main thread, and it allows the layer on top to not sync on the main thread.
## Follows the DOTS principles, is usable in DOTS Runtime and always compatible with the latest versions of the DOTS packages
There should always be a version compatible with the latest verions of the DOTS dependencies such as Unity Collections.
## The protocol is well defined and documented
Other implementations can communicate with games written with Unity Transport, without reverse engineering or reading the transport source code

7
Packages/com.unity.transport/DESIGN.md.meta


fileFormatVersion: 2
guid: 99b333ad37d614e42b1a4776de09e34e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

20
Packages/com.unity.transport/Documentation~/TableOfContents.md


# Unity Transport manual
* **Introduction**
* [Unity Transport overview](index.md)
* [Installation guide](install.md)
* **Workflows**
* [Creating a minimal client and server](workflow-client-server.md)
* [Creating a jobified client and server](workflow-client-server-jobs.md)
* [Using pipelines](pipelines-usage.md)
* **Background information**
* [Understanding the Update flow](update-flow.md)
* [Understanding the Connection State Machine](connection-state-machine.md)
* [Event consumption](event-consumption.md)
* **Samples**
* [ClientBehaviour](samples/clientbehaviour.cs.md)
* [ServerBehaviour](samples/serverbehaviour.cs.md)
* [JobifiedClientBehaviour](samples/jobifiedclientbehaviour.cs.md)
* [JobifiedServerBehaviour](samples/jobifiedserverbehaviour.cs.md)
* [Source Project for Workflows](https://oc.unity3d.com/index.php/s/PHaNZP79Va2YOLT)

17
Packages/com.unity.transport/Documentation~/connection-state-machine.md


# Understanding the Connection State Machine
It's important to at least understand how transitions occur in the connection state machine so you make decisions depending on what triggered each state. And to understand the subtle differences depending if you are `Connecting` to another host or if you simply want to Listen for incoming connections. As you can see below the state machine for the `NetworkConnection` is pretty simple.
![ConnectionState](images/com.unity.transport.connection.png)
All connections start in `Disconnected` state.
- Depending what state the `NetworkDriver` is in, the `Listening (Passive)` state might be triggered. This is when the driver acts like a server listening for incoming connections and data requests. And secondly you could try to use the driver to connect to a remote endpoint and then we would invoke another flow of the state machine.
So to give a overview we have two standard scenarios. Either you listen for incoming connections or you use and outgoing connection to connect to someone else.
In our [client/server workflow](workflow-client-server.md) we use the ServerBehaviour to `Listen` and the ClientBehaviour to `Connect`.
[Back to table of contents](TableOfContents.md)

60
Packages/com.unity.transport/Documentation~/event-consumption.md


# Event consumption
There are currently 4 types of events supplied by the `NetworkDriver`
```c#
public enum Type
{
Empty = 0,
Data,
Connect,
Disconnect
}
```
As mentioned, there are a few subtle differences running the driver as a host or client. Mainly when it comes to consumption of events.
Both your client and your server loop will want to consume the events that are produced by the `NetworkDriver`. And you do so by either calling `PopEvent` on each `NetworkConnection` similar to how we did before.
```c#
DataStreamReader strm;
NetworkEvent.Type cmd;
while ((cmd = m_Connection.PopEvent(driver, out strm)) != NetworkEvent.Type.Empty)
; // Handle Event
```
You can try calling the `PopEventForConnection` on the `NetworkDriver` as we did in the ServerBehaviour example:
```c#
DataStreamReader strm;
NetworkEvent.Type cmd;
while ((cmd = m_Driver.PopEventForConnection(m_Connections[i], out strm)) != NetworkEvent.Type.Empty)
; // Handle Event
```
There is no real difference between these calls, both calls will do the same thing. Its just how you want to phrase yourself when writing the code.
And finally to receive a new `NetworkConnection` on the Driver while Listening you can call `Accept`
```c#
NetworkConnection c;
while ((c = m_Driver.Accept()) != default(NetworkConnection))
; // Handle Connection Event.
```
| Event | Description |
| ---------- | ------------------------------------------------------------ |
| Empty | The `Empty` event signals that there are no more messages in our event queue to handle this frame. |
| Data | The `Data` event signals that we have received data from a connected endpoint. |
| Connect | The `Connect` event signals that a new connection has been established.<br> **Note**: this event is only available if the `NetworkDriver` is **not** in the `Listening` state. |
| Disconnect | The `Disconnect` event is received if;<br> 1. `Disconnect` packet was received (calling `NetworkConnection::Disconnect` will trigger this.)<br> 2. A *socket timeout* occurred.<br> 3. Maximum connect attempts on the `NetworkConnection` exceeded. <br> **Note:** That if you call `Disconnect` on your `NetworkConnection` this will **NOT** trigger an `Disconnect` event on your local `NetworkDriver`. |
Looking at this table we see that there are 2 things that stand out.
- The first thing is that the `Connect` event is only available if the `NetworkDriver` is **NOT** `Listening`
- In order to receive any `Connect` events on a `NetworkDriver` that is in the `Listening` state we need to call the special function `Accept` just as we did in the *Creating a Server* section in the [Creating a minimal client and server](workflow-client-server.md) workflow page.
- The second thing to notice is that if you call `Disconnect` on a `NetworkConnection` this will not trigger an event inside your own driver.
[Back to table of contents](TableOfContents.md)

94
Packages/com.unity.transport/Documentation~/images/Pipeline-stages-diagram.png

之前 之后
宽度: 520  |  高度: 224  |  大小: 14 KiB

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

正在加载...
取消
保存