浏览代码

Merging the UTP changes.

/main/staging
nathaniel.buck@unity3d.com 3 年前
当前提交
363b2f50
共有 90 个文件被更改,包括 3859 次插入1335 次删除
  1. 47
      Assets/Prefabs/UI/GameCanvas.prefab
  2. 883
      Assets/Prefabs/UI/JoinContent.prefab
  3. 43
      Assets/Prefabs/UI/JoinCreateCanvas.prefab
  4. 32
      Assets/Prefabs/UI/LobbyButtonUI.prefab
  5. 79
      Assets/Prefabs/UI/LobbyGameCanvas.prefab
  6. 868
      Assets/Prefabs/UI/PlayerInteractionPanel.prefab
  7. 7
      Assets/Prefabs/UI/RenamePopup.prefab
  8. 249
      Assets/Scenes/mainScene.unity
  9. 262
      Assets/Scripts/Entities/GameStateManager.cs
  10. 119
      Assets/Scripts/Entities/LobbyUser.cs
  11. 125
      Assets/Scripts/Entities/LocalLobby.cs
  12. 8
      Assets/Scripts/Infrastructure/Messenger.cs
  13. 6
      Assets/Scripts/Infrastructure/ObserverBehaviour.cs
  14. 4
      Assets/Scripts/Infrastructure/UpdateSlow.cs
  15. 23
      Assets/Scripts/Lobby/LobbyAPIInterface.cs
  16. 54
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  17. 53
      Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
  18. 1
      Assets/Scripts/Lobby/LobbyListHeartbeat.cs
  19. 67
      Assets/Scripts/Lobby/ToLocalLobby.cs
  20. 5
      Assets/Scripts/LobbyRelaySample.asmdef
  21. 20
      Assets/Scripts/Relay/RelayInterface.cs
  22. 233
      Assets/Scripts/Tests/PlayMode/LobbyReadyCheckTests.cs
  23. 16
      Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs
  24. 6
      Assets/Scripts/Tests/PlayMode/UpdateSlowTests.cs
  25. 12
      Assets/Scripts/UI/CountdownUI.cs
  26. 4
      Assets/Scripts/UI/EmoteButtonUI.cs
  27. 2
      Assets/Scripts/UI/EndGameButtonUI.cs
  28. 14
      Assets/Scripts/UI/InLobbyUserUI.cs
  29. 4
      Assets/Scripts/UI/JoinMenuUI.cs
  30. 2
      Assets/Scripts/UI/ReadyCheckUI.cs
  31. 15
      Assets/Scripts/UI/UIPanelBase.cs
  32. 4
      Packages/com.unity.services.lobby/CHANGELOG.md
  33. 43
      Packages/com.unity.services.lobby/Runtime/Apis/LobbyApi.cs
  34. 111
      Packages/com.unity.services.lobby/Runtime/Apis/LobbyApiRequests.cs
  35. 4
      Packages/com.unity.services.lobby/Runtime/Http/BaseApiClient.cs
  36. 14
      Packages/com.unity.services.lobby/Runtime/Http/DeserializationException.cs
  37. 54
      Packages/com.unity.services.lobby/Runtime/Http/HttpClient.cs
  38. 13
      Packages/com.unity.services.lobby/Runtime/Http/HttpException.cs
  39. 5
      Packages/com.unity.services.lobby/Runtime/Http/IHttpClient.cs
  40. 4
      Packages/com.unity.services.lobby/Runtime/Http/JsonHelpers.cs
  41. 58
      Packages/com.unity.services.lobby/Runtime/Http/ResponseHandler.cs
  42. 5
      Packages/com.unity.services.lobby/Runtime/LobbyServiceProvider.cs
  43. 13
      Packages/com.unity.services.lobby/Runtime/Models/CreateRequest.cs
  44. 29
      Packages/com.unity.services.lobby/Runtime/Models/DataObject.cs
  45. 13
      Packages/com.unity.services.lobby/Runtime/Models/Detail.cs
  46. 17
      Packages/com.unity.services.lobby/Runtime/Models/ErrorStatus.cs
  47. 10
      Packages/com.unity.services.lobby/Runtime/Models/JoinByCodeRequest.cs
  48. 20
      Packages/com.unity.services.lobby/Runtime/Models/Lobby.cs
  49. 11
      Packages/com.unity.services.lobby/Runtime/Models/Player.cs
  50. 24
      Packages/com.unity.services.lobby/Runtime/Models/PlayerDataObject.cs
  51. 13
      Packages/com.unity.services.lobby/Runtime/Models/PlayerUpdateRequest.cs
  52. 8
      Packages/com.unity.services.lobby/Runtime/Models/QueryFilter.cs
  53. 8
      Packages/com.unity.services.lobby/Runtime/Models/QueryOrder.cs
  54. 11
      Packages/com.unity.services.lobby/Runtime/Models/QueryRequest.cs
  55. 13
      Packages/com.unity.services.lobby/Runtime/Models/QueryResponse.cs
  56. 10
      Packages/com.unity.services.lobby/Runtime/Models/QuickJoinRequest.cs
  57. 15
      Packages/com.unity.services.lobby/Runtime/Models/UpdateRequest.cs
  58. 4
      Packages/com.unity.services.lobby/Runtime/Scheduler/ThreadHelper.cs
  59. 44
      Packages/com.unity.services.lobby/package.json
  60. 9
      Packages/manifest.json
  61. 90
      Packages/packages-lock.json
  62. 4
      ProjectSettings/ProjectVersion.txt
  63. 2
      Packages/com.unity.services.lobby/Runtime/Http/DeserializationSettings.cs.meta
  64. 17
      Assets/Scripts/Entities/EmoteType.cs
  65. 11
      Assets/Scripts/Entities/EmoteType.cs.meta
  66. 229
      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. 207
      Assets/Scripts/Relay/RelayUtpSetup.cs
  71. 11
      Assets/Scripts/Relay/RelayUtpSetup.cs.meta
  72. 39
      Assets/Scripts/UI/RecolorForLobbyType.cs
  73. 11
      Assets/Scripts/UI/RecolorForLobbyType.cs.meta
  74. 9
      Packages/com.unity.services.lobby/CONTRIBUTING.md
  75. 7
      Packages/com.unity.services.lobby/CONTRIBUTING.md.meta
  76. 15
      Packages/com.unity.services.lobby/Runtime/Http/DeserializationSettings.cs
  77. 53
      Packages/com.unity.services.lobby/Runtime/Http/JsonObject.cs
  78. 11
      Packages/com.unity.services.lobby/Runtime/Http/JsonObject.cs.meta
  79. 28
      Packages/com.unity.services.lobby/Runtime/Http/JsonObjectConverter.cs
  80. 11
      Packages/com.unity.services.lobby/Runtime/Http/JsonObjectConverter.cs.meta
  81. 34
      Packages/com.unity.services.lobby/Runtime/Http/ResponseDeserializationException.cs
  82. 11
      Packages/com.unity.services.lobby/Runtime/Http/ResponseDeserializationException.cs.meta
  83. 16
      ProjectSettings/BurstAotSettings_StandaloneWindows.json
  84. 6
      ProjectSettings/CommonBurstAotSettings.json
  85. 273
      Assets/Prefabs/UI/CountDownUI.prefab
  86. 7
      Assets/Prefabs/UI/CountDownUI.prefab.meta
  87. 11
      Assets/Scripts/Lobby/ReadyCheck.cs.meta
  88. 63
      Assets/Scripts/Lobby/ReadyCheck.cs
  89. 0
      /Packages/com.unity.services.lobby/Runtime/Http/DeserializationSettings.cs.meta

47
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: 5713552561910003945, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_fontSize
value: 31.3
value: 18
objectReference: {fileID: 0}
- target: {fileID: 5749901921003950368, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 6281383251298602143, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_fontSize
value: 31.3
value: 18
objectReference: {fileID: 0}
- target: {fileID: 6701676754128905643, guid: 247f79ab5aefc6d40bcbdade4d9467b7, type: 3}
propertyPath: m_AnchorMax.y

- 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

propertyPath: m_fontSize
value: 17.45
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
value: 1

objectReference: {fileID: 0}
- target: {fileID: 3638764283281205412, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_Size
value: 1
value: 0
value: 0
value: 0.9999999
objectReference: {fileID: 0}
- target: {fileID: 3675426980811071808, guid: 404728f5cffe43940b290121bd31f601, type: 3}
propertyPath: m_AnchorMax.x

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

43
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

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

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

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

79
Assets/Prefabs/UI/LobbyGameCanvas.prefab


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

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

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
ShowThisWhen: 2
--- !u!1 &4637522307789944801
GameObject:

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

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_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 466693923092094802}
m_RootOrder: 0

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

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:

- target: {fileID: 4102997489641105918, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_Name
value: RelayCodeCanvas
objectReference: {fileID: 0}
- target: {fileID: 7546827419935918100, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_fontSize
value: 18.2
objectReference: {fileID: 0}
- target: {fileID: 7676491730539518990, guid: 27536a164837c9141bbe1adf7ba37dde, type: 3}
propertyPath: m_Alpha

propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2987822160017223264, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_toRecolor.Array.size
value: 5
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: 2804283545537723312}
- target: {fileID: 3210254045315593125, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y
value: 0

propertyPath: m_AnchoredPosition.y
value: 0
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
value: 0

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5151586559654887469, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y
value: 0

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6687484736792569641, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_Enabled
value: 1
objectReference: {fileID: 0}
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

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}

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

7
Assets/Prefabs/UI/RenamePopup.prefab


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

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 28.6
m_fontSize: 24.15
m_fontSizeBase: 14
m_fontWeight: 400
m_enableAutoSizing: 1

m_Script: {fileID: 11500000, guid: 160dfc78f3641c94fbebe419be087996, type: 3}
m_Name:
m_EditorClassIdentifier:
m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
--- !u!1 &8624457953407905705
GameObject:

249
Assets/Scenes/mainScene.unity


m_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
--- !u!114 &284612038 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
m_PrefabInstance: {fileID: 7716713812904700119}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b4f7225f73bfe6a4d9133ee45ac9cd73, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &297599733 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 2932669033074858231, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}

m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
--- !u!114 &648562208 stripped
--- !u!114 &883450645 stripped
m_CorrespondingSourceObject: {fileID: 6939937855246394599, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_CorrespondingSourceObject: {fileID: 6961224259983233844, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
--- !u!114 &883450645 stripped
--- !u!114 &1014339014 stripped
m_CorrespondingSourceObject: {fileID: 6961224259983233844, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_CorrespondingSourceObject: {fileID: 7853401853899595651, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Script: {fileID: 11500000, guid: 5b3b588e7ae40ec4ca35fdb9404513ab, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1217229506 stripped

m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1511612118 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 2906467829199353422, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_PrefabInstance: {fileID: 2637199315837045693}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1583737884 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 857969996410458156, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_PrefabInstance: {fileID: 2637199315837045693}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1793980663 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 1122466553803535762, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_PrefabInstance: {fileID: 2637199315837045693}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1886099429 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 3845984648666374778, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}

objectReference: {fileID: 0}
- target: {fileID: 115037783493914682, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 26.6
value: 26.65
objectReference: {fileID: 0}
- target: {fileID: 269281908346235976, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 273761196813220610, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Target
value:
objectReference: {fileID: 284612038}
- target: {fileID: 273761196813220610, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName
value: SetLobbyColorFilter
objectReference: {fileID: 0}
- target: {fileID: 273761196813220610, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName
value: LobbyRelaySample.GameStateManager, LobbyRelaySample
objectReference: {fileID: 0}
- target: {fileID: 273761196813220610, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_IntArgument
value: 3
objectReference: {fileID: 0}
- target: {fileID: 326167899787007624, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 505980609262872673, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 26.6
value: 26.65
objectReference: {fileID: 0}
- target: {fileID: 594314551941645310, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 774903988744295172, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 15
objectReference: {fileID: 0}
- target: {fileID: 901738327287436208, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 1582058628584328142, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 12.85
value: 14.9
objectReference: {fileID: 0}
- target: {fileID: 1587533333931548819, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize

propertyPath: m_fontSize
value: 20.55
objectReference: {fileID: 0}
- target: {fileID: 4215266910727840129, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Target
value:
objectReference: {fileID: 284612038}
- target: {fileID: 4215266910727840129, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName
value: SetLobbyColorFilter
objectReference: {fileID: 0}
- target: {fileID: 4215266910727840129, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName
value: LobbyRelaySample.GameStateManager, LobbyRelaySample
objectReference: {fileID: 0}
- target: {fileID: 4215266910727840129, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_IntArgument
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4461176893399258486, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

objectReference: {fileID: 0}
- target: {fileID: 4822032080772604407, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: -0.00005787611
value: -0.00000059604645
value: 0.0000018929122
value: -0.000048954073
objectReference: {fileID: 0}
- target: {fileID: 4824240073023402834, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_BlocksRaycasts
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5573036458859714118, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5573036458859714118, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5573036458859714118, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5573036458859714118, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5784287561701313075, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5784287561701313075, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5784287561701313075, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5784287561701313075, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5784287561701313075, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5784287561701313075, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5835473579278904550, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

- target: {fileID: 6221486310396140244, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_Alpha
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6350074270247024827, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Target
value:
objectReference: {fileID: 284612038}
- target: {fileID: 6350074270247024827, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName
value: SetLobbyColorFilter
objectReference: {fileID: 0}
- target: {fileID: 6350074270247024827, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName
value: LobbyRelaySample.GameStateManager, LobbyRelaySample
objectReference: {fileID: 0}
- target: {fileID: 6366899791732448022, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6656827769630762920, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_Alpha
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6664893525864283123, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: -19.000122

- target: {fileID: 6843940383602865416, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6910946530474729345, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_Alpha
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6970381027661675607, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 7824982990876343133, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 18
value: 22.35
objectReference: {fileID: 0}
- target: {fileID: 7864410018658149118, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

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

propertyPath: observeOnStart
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8204116518780172302, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Target
value:
objectReference: {fileID: 284612038}
- target: {fileID: 8204116518780172302, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName
value: SetLobbyColorFilter
objectReference: {fileID: 0}
- target: {fileID: 8204116518780172302, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName
value: LobbyRelaySample.GameStateManager, LobbyRelaySample
objectReference: {fileID: 0}
- target: {fileID: 8206858806911994013, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 18

propertyPath: m_fontSize
value: 35.8
objectReference: {fileID: 0}
- target: {fileID: 8428070622252142379, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 18
objectReference: {fileID: 0}
- target: {fileID: 8726787022963647593, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: showing
value: 1

- target: {fileID: 8740957582409016033, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8806225834216904879, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 18
objectReference: {fileID: 0}
- target: {fileID: 8889734615304832804, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045699 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5971689111536305866, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_PrefabInstance: {fileID: 2637199315837045693}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a03b37d5b8df06948b36dfbc430a1ea5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1001 &7716713812904700119
PrefabInstance:
m_ObjectHideFlags: 0

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

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

- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalUserObservers.Array.data[3]
value:
objectReference: {fileID: 0}
objectReference: {fileID: 1583737884}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalUserObservers.Array.data[4]
value:
objectReference: {fileID: 1793980663}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalUserObservers.Array.data[5]
value:
objectReference: {fileID: 2637199315837045699}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[0]
value:

value:
objectReference: {fileID: 648562208}
objectReference: {fileID: 1014339014}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[2]
value:

value:
objectReference: {fileID: 2126854580}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[9]
value:
objectReference: {fileID: 1511612118}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LobbyServerObservers.Array.data[0]
value:
objectReference: {fileID: 2637199315837045694}

objectReference: {fileID: 0}
- target: {fileID: 7716713811812636911, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalPosition.x
value: 624.1824
value: 0
value: 320.50745
value: 0
value: -29748.68
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636911, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalRotation.w

262
Assets/Scripts/Entities/GameStateManager.cs


using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Services.Relay.Models;
// TODO: This is pretty bloated. Additionally, it needs a pass for removing redundant calls and organizing things in a more intuitive way and whatnot
public class GameStateManager : MonoBehaviour, IReceiveMessages
{
[SerializeField]

LocalLobby m_localLobby;
LobbyServiceData m_lobbyServiceData = new LobbyServiceData();
LocalGameState m_localGameState = new LocalGameState();
ReadyCheck m_ReadyCheck;
RelayUtpSetup m_relaySetup;
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;
// Do some arbitrary operations to instantiate singletons.
// Do some arbitrary operations to instantiate singletons.
m_ReadyCheck = new ReadyCheck(7);
Application.wantsToQuit += OnWantToQuit;
}

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.
/// <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)

else if (type == MessageType.CreateLobbyRequest)
{
var createLobbyData = (LocalLobby)msg;
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, (r) =>
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) =>
lobby.ToLocalLobby.Convert(r, m_localLobby, m_localUser);
lobby.ToLocalLobby.Convert(r, m_localLobby);
LobbyInfo lobbyInfo = (LobbyInfo)msg;
LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, (r) =>
LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg;
LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, m_localUser, (r) =>
lobby.ToLocalLobby.Convert(r, m_localLobby, m_localUser);
lobby.ToLocalLobby.Convert(r, m_localLobby);
OnJoinedLobby();
}, OnFailedJoin);
}

if (er != null)
errorLong = er.Status;
OnRefreshFailed(errorLong);
});
},
m_lobbyColorFilter);
}
else if (type == MessageType.ChangeGameState)
{

{
var emote = (string)msg;
EmoteType emote = (EmoteType)msg;
else if (type == MessageType.ChangeLobbyUserState)
else if (type == MessageType.LobbyUserStatus)
else if (type == MessageType.Client_EndReadyCountdownAt)
else if (type == MessageType.StartCountdown)
m_localLobby.TargetEndTime = (DateTime)msg;
else if (type == MessageType.ToLobby)
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)
ToLobby();
m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
SetUserLobbyState();
m_localLobby = new LocalLobby
{
State = LobbyState.Lobby
};
m_localLobby = new LocalLobby { State = LobbyState.Lobby };
DefaultObserverSetup();
InitObservers();
BeginObservers();
/// <summary>
/// We find and validate that the scene has all the Observers we expect
/// </summary>
void DefaultObserverSetup()
{
foreach (var gameStateObs in FindObjectsOfType<LocalGameStateObserver>())
{
if (!gameStateObs.observeOnStart)
continue;
if (!m_GameStateObservers.Contains(gameStateObs))
m_GameStateObservers.Add(gameStateObs);
}
foreach (var localLobby in FindObjectsOfType<LocalLobbyObserver>())
{
if (!localLobby.observeOnStart)
continue;
if (!m_LocalLobbyObservers.Contains(localLobby))
m_LocalLobbyObservers.Add(localLobby);
}
foreach (var lobbyUserObs in FindObjectsOfType<LobbyUserObserver>())
{
if (!lobbyUserObs.observeOnStart)
continue;
if (!m_LocalUserObservers.Contains(lobbyUserObs))
m_LocalUserObservers.Add(lobbyUserObs);
}
foreach (var lobbyServiceObs in FindObjectsOfType<LobbyServiceDataObserver>())
{
if (!lobbyServiceObs.observeOnStart)
continue;
if (!m_LobbyServiceObservers.Contains(lobbyServiceObs))
m_LobbyServiceObservers.Add(lobbyServiceObs);
}
if (m_GameStateObservers.Count < 4)
Debug.LogWarning($"Scene has less than the default expected Game State Observers, ensure all the observers in the scene that need to watch the gameState are registered in the LocalGameStateObservers List.");
if (m_LocalLobbyObservers.Count < 8)
Debug.LogWarning($"Scene has less than the default expected Local Lobby Observers, ensure all the observers in the scene that need to watch the Local Lobby are registered in the LocalLobbyObservers List.");
if (m_LocalUserObservers.Count < 3)
Debug.LogWarning($"Scene has less than the default expected Local User Observers, ensure all the observers in the scene that need to watch the gameState are registered in the LocalUserObservers List.");
if (m_LobbyServiceObservers.Count < 2)
Debug.LogWarning($"Scene has less than the default expected Lobby Service Observers, ensure all the observers in the scene that need to watch the lobby service state are registered in the LobbyServiceObservers List.");
}
void InitObservers()
void BeginObservers()
{
if (gameStateObs == null)
{
Debug.LogError("Missing a gameStateObserver, please make sure all GameStateObservers in the scene are registered here.");
continue;
}
}
foreach (var serviceObs in m_LobbyServiceObservers)
serviceObs.BeginObserving(m_lobbyServiceData);
{
if (lobbyObs == null)
{
Debug.LogError("Missing a gameStateObserver, please make sure all GameStateObservers in the scene are registered here.");
continue;
}
}
{
if (userObs == null)
{
Debug.LogError("Missing a gameStateObserver, please make sure all GameStateObservers in the scene are registered here.");
continue;
}
}
foreach (var serviceObs in m_LobbyServiceObservers)
{
if (serviceObs == null)
{
Debug.LogError("Missing a gameStateObserver, please make sure all GameStateObservers in the scene are registered here.");
continue;
}
serviceObs.BeginObserving(m_lobbyServiceData);
}
}
void SetGameState(GameState state)

void OnCreatedLobby()
{
m_localUser.IsHost = true;
void OnGotRelayAllocation(Allocation allocationID)
void OnJoinedLobby()
RelayInterface.GetJoinCodeAsync(allocationID.AllocationId, OnGotRelayCode);
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
SetUserLobbyState();
StartRelayConnection();
void OnGotRelayCode(string relayCode)
void StartRelayConnection()
m_localLobby.RelayCode = relayCode;
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);
}
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);
void OnJoinedLobby()
IEnumerator RetryRelayConnection()
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
SetUserLobbyState();
Dictionary<string, string> displayNameData = new Dictionary<string, string>();
displayNameData.Add("DisplayName", m_localUser.DisplayName);
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(displayNameData, null);
yield return new WaitForSeconds(5);
StartRelayConnection();
m_localUser.Emote = null;
m_localUser.ResetState();
if (m_relaySetup != null)
{ Component.Destroy(m_relaySetup);
m_relaySetup = null;
}
if (m_relayClient != null)
{ Component.Destroy(m_relayClient);
m_relayClient = null;
}
}
/// <summary>

{
SetGameState(GameState.JoinMenu);
}
// Only start the countdown once.
// We want to do all the Relay Allocation calls in quick succession, as waiting too long
// (10s) will cause the Relay server to get cleaned up by the service
RelayInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnGotRelayAllocation);
m_localLobby.CountDownTime = m_localLobby.TargetEndTime.Subtract(DateTime.Now).Seconds;
m_localLobby.CountDownTime = 4;
/// This is currently a countdown to Connection, once we have our transport integrated, this will be a countdown to Game Start
/// 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).
m_ReadyCheck.EndCheckingForReady();
yield return new WaitForSeconds(0.2f);
yield return null;
m_localLobby.CountDownTime = m_localLobby.TargetEndTime.Subtract(DateTime.Now).Seconds;
m_localLobby.CountDownTime -= Time.deltaTime;
m_localUser.UserStatus = UserStatus.Connecting;
m_localLobby.State = LobbyState.InGame;
// TODO TRANSPORT: Move Relay Join to Pre-Countdown, and do connection and health checks before counting down for the game start.
RelayInterface.JoinAsync(m_localLobby.RelayCode, OnJoinedRelay);
}
/// <summary>
/// Non Hosts Connect to server Here
/// </summary>
void OnJoinedRelay(JoinAllocation joinData)
{
m_localUser.UserStatus = UserStatus.Connected;
var ip = joinData.RelayServer.IpV4;
var port = joinData.RelayServer.Port;
m_localLobby.RelayServer = new ServerAddress(ip, port);
}
void ToLobby()
{
m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
m_localLobby.RelayServer = null;
m_localLobby.RelayCode = null;
SetUserLobbyState();
if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
m_localUser.UserStatus = UserStatus.Lobby;
if (m_localUser.IsHost)
m_ReadyCheck.BeginCheckingForReady();
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
m_localLobby.CopyObserved(new LobbyInfo(), new Dictionary<string, LobbyUser>());
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_ReadyCheck.EndCheckingForReady();
}
void OnDestroy()

119
Assets/Scripts/Entities/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;
m_userStatus = value;
m_lastChanged = UserMembers.UserStatus;
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);
}
}

125
Assets/Scripts/Entities/LocalLobby.cs


namespace LobbyRelaySample
{
[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 }
/// <summary>
/// A local wrapper around a lobby's remote data, with additional functionality for providing that data to UI elements and tracking local player objects.

public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers;
#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); }
}
public DateTime TargetEndTime
float m_CountDownTime;
public float CountDownTime
get => m_TargetEndTime;
get { return m_CountDownTime; }
m_TargetEndTime = value;
m_CountDownTime = value;
OnChanged(this);
}
}

}
}
public long? AllPlayersReadyTime => m_data.AllPlayersReadyTime;
public LobbyColor Color
{
get => m_data.Color;
set
{
m_data.Color = value;
OnChanged(this);
}
}
/// <summary>
/// Checks if we have n players that have the Status.

public bool PlayersOfState(UserStatus status, int playersCount = -1)
public bool PlayersOfState(UserStatus status, int playersCount = -1) // TODO: Remove test-only API.
{
var statePlayers = m_LobbyUsers.Values.Count(user => user.UserStatus == status);

}
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);

8
Assets/Scripts/Infrastructure/Messenger.cs


PlayerJoinedLobby = 5,
PlayerLeftLobby = 6,
ChangeGameState = 7,
ChangeLobbyUserState = 8,
LobbyUserStatus = 8,
ToLobby = 12,
Client_EndReadyCountdownAt = 13,
EndGame = 12,
StartCountdown = 13,
CancelCountdown = 14,
ConfirmInGameState = 15,
}
/// <summary>

6
Assets/Scripts/Infrastructure/ObserverBehaviour.cs


public abstract class ObserverBehaviour<T> : MonoBehaviour where T : Observed<T>
{
public T observed { get; set; }
/// <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)
{

4
Assets/Scripts/Infrastructure/UpdateSlow.cs


}
/// <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.
/// Some might also not want to be coupled to a Unity object at all but still need an update loop.
/// </summary>
public class UpdateSlow : MonoBehaviour, IUpdateSlow

while (m_updateTimer > effectivePeriod)
{
m_updateTimer -= effectivePeriod;
OnUpdate(effectivePeriod);
OnUpdate(m_updatePeriod); // Using m_updatePeriod will be incorrect on the first update for a new subscriber, due to the staggering. However, we don't expect UpdateSlow subscribers to require precision, and this is less verbose than tracking per-subscriber.
}
}

23
Assets/Scripts/Lobby/LobbyAPIInterface.cs


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 CreateLobbyAsync(string requesterUASId, string lobbyName, int maxPlayers, bool isPrivate, Dictionary<string, PlayerDataObject> localUserData, Action<Response<Lobby>> onComplete)
player: new Player(requesterUASId),
player: new Player(id: requesterUASId, data: localUserData),
maxPlayers: maxPlayers,
isPrivate: isPrivate
));

new InProgressRequest<Response>(task, onComplete);
}
public static void JoinLobbyAsync_ByCode(string requesterUASId, string lobbyCode, Action<Response<Lobby>> onComplete)
public static void JoinLobbyAsync_ByCode(string requesterUASId, string lobbyCode, Dictionary<string, PlayerDataObject> localUserData, Action<Response<Lobby>> onComplete)
JoinLobbyByCodeRequest joinRequest = new JoinLobbyByCodeRequest(new JoinByCodeRequest(lobbyCode, new Player(requesterUASId)));
JoinLobbyByCodeRequest joinRequest = new JoinLobbyByCodeRequest(new JoinByCodeRequest(lobbyCode, new Player(id: requesterUASId, data: localUserData)));
public static void JoinLobbyAsync_ById(string requesterUASId, string lobbyId, Action<Response<Lobby>> onComplete)
public static void JoinLobbyAsync_ById(string requesterUASId, string lobbyId, Dictionary<string, PlayerDataObject> localUserData, Action<Response<Lobby>> onComplete)
JoinLobbyByIdRequest joinRequest = new JoinLobbyByIdRequest(lobbyId, new Player(requesterUASId));
JoinLobbyByIdRequest joinRequest = new JoinLobbyByIdRequest(lobbyId, new Player(id: requesterUASId, data: localUserData));
var task = LobbyService.LobbyApiClient.JoinLobbyByIdAsync(joinRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
}

new InProgressRequest<Response>(task, onComplete);
}
public static void QueryAllLobbiesAsync(Action<Response<QueryResponse>> onComplete)
public static void QueryAllLobbiesAsync(List<QueryFilter> filters, Action<Response<QueryResponse>> onComplete)
QueryLobbiesRequest queryRequest = new QueryLobbiesRequest(new QueryRequest(count: k_maxLobbiesToShow));
QueryLobbiesRequest queryRequest = new QueryLobbiesRequest(new QueryRequest(count: k_maxLobbiesToShow, filter: filters));
var task = LobbyService.LobbyApiClient.QueryLobbiesAsync(queryRequest);
new InProgressRequest<Response<QueryResponse>>(task, onComplete);
}

));
var task = LobbyService.LobbyApiClient.UpdatePlayerAsync(updateRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
}
public static void HeartbeatPlayerAsync(string lobbyId)
{
HeartbeatRequest request = new HeartbeatRequest(lobbyId);
var task = LobbyService.LobbyApiClient.HeartbeatAsync(request);
new InProgressRequest<Response>(task, null);
}
}
}

54
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


#endregion
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)
{

}
/// <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)
public void JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser, Action<Lobby> onSuccess, Action onFailure)
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)
{

/// <summary>Used for getting the list of all active lobbies, without needing full info for each.</summary>
/// <param name="onListRetrieved">If called with null, retrieval was unsuccessful. Else, this will be given a list of contents to display, as pairs of a lobby code and a display string for that lobby.</param>
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<Response<QueryResponse>> onError = null)
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<Response<QueryResponse>> onError = null, LobbyColor limitToColor = LobbyColor.None)
LobbyAPIInterface.QueryAllLobbiesAsync(OnLobbyListRetrieved);
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(Response<QueryResponse> response)
{

void OnLeftLobby(Response response)
{
onComplete?.Invoke();
// 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);
}
}

Dictionary<string, DataObject> dataCurr = lobby.Data ?? new Dictionary<string, DataObject>();
foreach (var dataNew in data)
{
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.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else

return false;
}
return true;
}
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);
}
}
}
}

53
Assets/Scripts/Lobby/LobbyContentHeartbeat.cs


using System;
using System.Collections.Generic;
// TODO: It might make sense to change UpdateSlow to, rather than have a fixed cycle on which everything is bound, be able to track when each thing should update?
// I.e. what I want here now is for when a lobby async request comes in, if it has already been long enough, it immediately fires and then sets a cooldown.
// This is still necessary for detecting new players, although I think we could hit a case where the relay join ends up coming in before the cooldown?
// So, we should be able to create a new LobbyUser that way as well.
// That is, creating a (local) player via Relay or via Lobby should go through the same mechanism...? Or do we hold onto the Relay data until the player exists?
/// <summary>
/// Keep updated on changes to a joined lobby.
/// </summary>

{
if (m_isAwaitingQuery || m_localLobby == null)
return;
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;
}
}
}

1
Assets/Scripts/Lobby/LobbyListHeartbeat.cs


/// </summary>
public class LobbyListHeartbeat : MonoBehaviour
{
// This is called in-editor via events.
public void SetActive(bool isActive)
{
if (isActive)

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,
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby,
AllPlayersReadyTime = lobby.Data?.ContainsKey("AllPlayersReady") == true ? long.Parse(lobby.Data["AllPlayersReady"].Value) : (long?)null
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null, // TODO: Remove?
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby, // TODO: Consider TryParse, just in case (and below). Although, we don't have fail logic anyway...
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, or if we just don't know about them yet, get the most recent data that the lobby knows.
// (If we have no local representation of the player, that gets 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);
outputToHere.CopyObserved(info, lobbyUsers);
}

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,

20
Assets/Scripts/Relay/RelayInterface.cs


private async void DoRequest(Task<T> task, Action<T> onComplete)
{
T result = await task;
onComplete?.Invoke(result);
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);
}
// TODO: Ensure that passing null as result is handled.
}
}

{
AllocateAsync(maxConnections, a =>
{
if (a.Status >= 200 && a.Status < 300)
if (a == null)
Debug.LogError("Relay returned a null Allocation. It's possible the Relay service is currently down.");
else if (a.Status >= 200 && a.Status < 300)
{
}
});
}

233
Assets/Scripts/Tests/PlayMode/LobbyReadyCheckTests.cs


using LobbyRelaySample;
using NUnit.Framework;
using System.Collections;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
using UnityEngine.TestTools;
using LobbyAPIInterface = LobbyRelaySample.lobby.LobbyAPIInterface;
// TODO: Obsolete. Replace with something accurate to Relay with transport.
namespace Test
{
public class LobbyReadyCheckTests
{
private string m_workingLobbyId;
private LobbyRelaySample.Auth.Identity m_auth;
private bool m_didSigninComplete = false;
private GameObject m_updateSlowObj;
//using LobbyRelaySample;
//using NUnit.Framework;
//using System.Collections;
//using Unity.Services.Lobbies;
//using Unity.Services.Lobbies.Models;
//using UnityEngine;
//using UnityEngine.TestTools;
//using LobbyAPIInterface = LobbyRelaySample.lobby.LobbyAPIInterface;
//namespace Test
//{
// public class LobbyReadyCheckTests
// {
// private string m_workingLobbyId;
// private LobbyRelaySample.Auth.Identity m_auth;
// private bool m_didSigninComplete = false;
// private GameObject m_updateSlowObj;
[OneTimeSetUp]
public void Setup()
{
m_auth = new LobbyRelaySample.Auth.Identity(() => { m_didSigninComplete = true; });
Locator.Get.Provide(m_auth);
m_updateSlowObj = new GameObject("UpdateSlowTest");
m_updateSlowObj.AddComponent<UpdateSlow>();
}
// [OneTimeSetUp]
// public void Setup()
// {
// m_auth = new LobbyRelaySample.Auth.Identity(() => { m_didSigninComplete = true; });
// Locator.Get.Provide(m_auth);
// m_updateSlowObj = new GameObject("UpdateSlowTest");
// m_updateSlowObj.AddComponent<UpdateSlow>();
// }
[UnityTearDown]
public IEnumerator PerTestTeardown()
{
if (m_workingLobbyId != null)
{ LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, null);
m_workingLobbyId = null;
}
yield return new WaitForSeconds(0.5f); // We need a yield anyway, so wait long enough to probably delete the lobby. There currently (6/22/2021) aren't other tests that would have issues if this took longer.
}
// [UnityTearDown]
// public IEnumerator PerTestTeardown()
// {
// if (m_workingLobbyId != null)
// { LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, null);
// m_workingLobbyId = null;
// }
// yield return new WaitForSeconds(0.5f); // We need a yield anyway, so wait long enough to probably delete the lobby. There currently (6/22/2021) aren't other tests that would have issues if this took longer.
// }
[OneTimeTearDown]
public void Teardown()
{
Locator.Get.Provide(new LobbyRelaySample.Auth.IdentityNoop());
m_auth.Dispose();
LogAssert.ignoreFailingMessages = false;
LobbyAsyncRequests.Instance.EndTracking();
GameObject.Destroy(m_updateSlowObj);
}
// [OneTimeTearDown]
// public void Teardown()
// {
// Locator.Get.Provide(new LobbyRelaySample.Auth.IdentityNoop());
// m_auth.Dispose();
// LogAssert.ignoreFailingMessages = false;
// LobbyAsyncRequests.Instance.EndTracking();
// GameObject.Destroy(m_updateSlowObj);
// }
private IEnumerator WaitForSignin()
{
// Wait a reasonable amount of time for sign-in to complete.
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
}
// private IEnumerator WaitForSignin()
// {
// // Wait a reasonable amount of time for sign-in to complete.
// if (!m_didSigninComplete)
// yield return new WaitForSeconds(3);
// if (!m_didSigninComplete)
// Assert.Fail("Did not sign in.");
// }
private IEnumerator CreateLobby(string lobbyName, string userId)
{
Response<Lobby> createResponse = null;
float timeout = 5;
LobbyAPIInterface.CreateLobbyAsync(userId, lobbyName, 4, false, (r) => { createResponse = r; });
while (createResponse == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
Assert.Greater(timeout, 0, "Timeout check (lobby creation).");
m_workingLobbyId = createResponse.Result.Id;
}
// private IEnumerator CreateLobby(string lobbyName, string userId)
// {
// Response<Lobby> createResponse = null;
// float timeout = 5;
// LobbyAPIInterface.CreateLobbyAsync(userId, lobbyName, 4, false, (r) => { createResponse = r; });
// while (createResponse == null && timeout > 0)
// { yield return new WaitForSeconds(0.25f);
// timeout -= 0.25f;
// }
// Assert.Greater(timeout, 0, "Timeout check (lobby creation).");
// m_workingLobbyId = createResponse.Result.Id;
// }
private IEnumerator PushPlayerData(LobbyUser player)
{
bool hasPushedPlayerData = false;
float timeout = 5;
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(LobbyRelaySample.lobby.ToLocalLobby.RetrieveUserData(player), () => { hasPushedPlayerData = true; }); // LobbyContentHeartbeat normally does this.
while (!hasPushedPlayerData && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
Assert.Greater(timeout, 0, "Timeout check (push player data).");
}
// private IEnumerator PushPlayerData(LobbyUser player)
// {
// bool hasPushedPlayerData = false;
// float timeout = 5;
// LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(LobbyRelaySample.lobby.ToLocalLobby.RetrieveUserData(player), () => { hasPushedPlayerData = true; }); // LobbyContentHeartbeat normally does this.
// while (!hasPushedPlayerData && timeout > 0)
// { yield return new WaitForSeconds(0.25f);
// timeout -= 0.25f;
// }
// Assert.Greater(timeout, 0, "Timeout check (push player data).");
// }
/// <summary>
/// After creating a lobby and a player, signal that the player is Ready. This should lead to a countdown time being set for all players.
/// </summary>
[UnityTest]
public IEnumerator SetCountdownTimeSinglePlayer()
{
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.
ReadyCheck readyCheck = new ReadyCheck(5); // This ready time is used for the countdown target end, not for any of the timing of actually detecting readies.
yield return WaitForSignin();
// /// <summary>
// /// After creating a lobby and a player, signal that the player is Ready. This should lead to a countdown time being set for all players.
// /// </summary>
// [UnityTest]
// public IEnumerator SetCountdownTimeSinglePlayer()
// {
// 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.
// ReadyCheck readyCheck = new ReadyCheck(5); // This ready time is used for the countdown target end, not for any of the timing of actually detecting readies.
// yield return WaitForSignin();
string userId = m_auth.GetSubIdentity(LobbyRelaySample.Auth.IIdentityType.Auth).GetContent("id");
yield return CreateLobby("TestReadyLobby1", userId);
// string userId = m_auth.GetSubIdentity(LobbyRelaySample.Auth.IIdentityType.Auth).GetContent("id");
// yield return CreateLobby("TestReadyLobby1", userId);
LobbyAsyncRequests.Instance.BeginTracking(m_workingLobbyId);
yield return new WaitForSeconds(2); // Allow the initial lobby retrieval.
// LobbyAsyncRequests.Instance.BeginTracking(m_workingLobbyId);
// yield return new WaitForSeconds(2); // Allow the initial lobby retrieval.
LobbyUser user = new LobbyUser();
user.ID = userId;
user.UserStatus = UserStatus.Ready;
yield return PushPlayerData(user);
// LobbyUser user = new LobbyUser();
// user.ID = userId;
// user.UserStatus = UserStatus.Ready;
// yield return PushPlayerData(user);
readyCheck.BeginCheckingForReady();
float timeout = 5; // Long enough for two slow updates
yield return new WaitForSeconds(timeout);
// readyCheck.BeginCheckingForReady();
// float timeout = 5; // Long enough for two slow updates
// yield return new WaitForSeconds(timeout);
readyCheck.Dispose();
LobbyAsyncRequests.Instance.EndTracking();
// readyCheck.Dispose();
// LobbyAsyncRequests.Instance.EndTracking();
yield return new WaitForSeconds(2); // Buffer to prevent a 429 on the upcoming Get, since there's a Get request on the slow upate loop when that's active.
Response<Lobby> getResponse = null;
timeout = 5;
LobbyAPIInterface.GetLobbyAsync(m_workingLobbyId, (r) => { getResponse = r; });
while (getResponse == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;
}
Assert.Greater(timeout, 0, "Timeout check (get lobby).");
Assert.NotNull(getResponse.Result, "Retrieved lobby successfully.");
Assert.NotNull(getResponse.Result.Data, "Lobby should have data.");
// yield return new WaitForSeconds(2); // Buffer to prevent a 429 on the upcoming Get, since there's a Get request on the slow upate loop when that's active.
// Response<Lobby> getResponse = null;
// timeout = 5;
// LobbyAPIInterface.GetLobbyAsync(m_workingLobbyId, (r) => { getResponse = r; });
// while (getResponse == null && timeout > 0)
// { yield return new WaitForSeconds(0.25f);
// timeout -= 0.25f;
// }
// Assert.Greater(timeout, 0, "Timeout check (get lobby).");
// Assert.NotNull(getResponse.Result, "Retrieved lobby successfully.");
// Assert.NotNull(getResponse.Result.Data, "Lobby should have data.");
Assert.True(getResponse.Result.Data.ContainsKey("AllPlayersReady"), "Check for AllPlayersReady key.");
string readyString = getResponse.Result.Data["AllPlayersReady"]?.Value;
Assert.NotNull(readyString, "Check for non-null AllPlayersReady.");
Assert.True(long.TryParse(readyString, out long ticks), "Check for ticks value in AllPlayersReady."); // This will be based on the current time, so we won't check for a specific value.
}
// Assert.True(getResponse.Result.Data.ContainsKey("AllPlayersReady"), "Check for AllPlayersReady key.");
// string readyString = getResponse.Result.Data["AllPlayersReady"]?.Value;
// Assert.NotNull(readyString, "Check for non-null AllPlayersReady.");
// Assert.True(long.TryParse(readyString, out long ticks), "Check for ticks value in AllPlayersReady."); // This will be based on the current time, so we won't check for a specific value.
// }
// Can't test with multiple players on one machine, since anonymous UAS credentials can't be manually supplied.
}
}
// // Can't test with multiple players on one machine, since anonymous UAS credentials can't be manually supplied.
// }
//}

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


using NUnit.Framework;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;

{
/// <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.
/// The actual code accessing lobbies should go through LobbyAsyncRequests. This serves to ensure the connection to the Lobby service is functional.
/// </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]

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;
float timeout = 5;
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; });
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponse = qr; });
while (queryResponse == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;

Response<Lobby> createResponse = null;
timeout = 5;
string lobbyName = "TestLobby-JustATest-123";
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; });
while (createResponse == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;

yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.
queryResponse = null;
timeout = 5;
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; });
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponse = qr; });
while (queryResponse == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;

yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.
Response<QueryResponse> queryResponseTwo = null;
timeout = 5;
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponseTwo = qr; });
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponseTwo = qr; });
while (queryResponseTwo == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;

Assert.Fail("Did not sign in.");
bool? didComplete = null;
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);

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


private class Subscriber : IDisposable
{
private Action m_thingToDo;
public float prevDt;
public Subscriber(Action thingToDo)
{

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

yield return new WaitForSeconds(0.1f);
Assert.AreEqual(1, updateCount, "Slow update period should have passed.");
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta.");
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta again.");
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta with two subscribers.");
Assert.AreEqual(1.5f, sub2.prevDt, "Slow update should have received the full time delta on the second subscriber as well.");
sub2.Dispose();
yield return new WaitForSeconds(k_period);

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);
}
}
}

14
Assets/Scripts/UI/InLobbyUserUI.cs


{
m_DisplayNameText.SetText(observed.DisplayName);
m_StatusText.SetText(SetStatusFancy(observed.UserStatus));
m_EmoteText.SetText(observed.Emote);
m_EmoteText.SetText(observed.Emote.GetString());
}
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 "";
}
}
}

4
Assets/Scripts/UI/JoinMenuUI.cs


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)
{
if (!string.IsNullOrEmpty(newCode))
m_LocalLobbySelected = new LobbyInfo(newCode.ToUpper());
m_LocalLobbySelected = new LocalLobby.LobbyData(newCode.ToUpper());
}
public void OnJoinButtonPressed()

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/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()

MyCanvasGroup.blocksRaycasts = false;
showing = false;
m_onVisibilityChange?.Invoke(false);
foreach (UIPanelBase child in m_uiPanelsInChildren)
child.m_onVisibilityChange?.Invoke(false);
}
}
}

4
Packages/com.unity.services.lobby/CHANGELOG.md


The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [0.2.1-preview] - 2021-07-22
Regenerate after adding Heartbeat API.
## [0.2.0-preview] - 2021-07-08
Add preview tag.

43
Packages/com.unity.services.lobby/Runtime/Apis/LobbyApi.cs


/// Create a lobby
/// </summary>
/// <param name="request">Request object for CreateLobby</param>
/// <param name="operationConfiguration">Configuration for CreateLobby</param>
/// <returns>Task for a Response object containing status code, headers, and Lobby object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<Lobby>> CreateLobbyAsync(CreateLobbyRequest request, Configuration operationConfiguration = null);

/// Delete a lobby
/// </summary>
/// <param name="request">Request object for DeleteLobby</param>
/// <param name="operationConfiguration">Configuration for DeleteLobby</param>
/// <returns>Task for a Response object containing status code, headers</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response> DeleteLobbyAsync(DeleteLobbyRequest request, Configuration operationConfiguration = null);

/// Get lobby details
/// </summary>
/// <param name="request">Request object for GetLobby</param>
/// <param name="operationConfiguration">Configuration for GetLobby</param>
/// <returns>Task for a Response object containing status code, headers, and Lobby object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<Lobby>> GetLobbyAsync(GetLobbyRequest request, Configuration operationConfiguration = null);

/// Heartbeat a lobby
/// </summary>
/// <param name="request">Request object for Heartbeat</param>
/// <param name="operationConfiguration">Configuration for Heartbeat</param>
/// <returns>Task for a Response object containing status code, headers</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response> HeartbeatAsync(HeartbeatRequest request, Configuration operationConfiguration = null);
/// <summary>
/// Async Operation.
/// <param name="operationConfiguration">Configuration for JoinLobbyByCode</param>
/// <returns>Task for a Response object containing status code, headers, and Lobby object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<Lobby>> JoinLobbyByCodeAsync(JoinLobbyByCodeRequest request, Configuration operationConfiguration = null);

/// Join a lobby with lobby ID
/// </summary>
/// <param name="request">Request object for JoinLobbyById</param>
/// <param name="operationConfiguration">Configuration for JoinLobbyById</param>
/// <returns>Task for a Response object containing status code, headers, and Lobby object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<Lobby>> JoinLobbyByIdAsync(JoinLobbyByIdRequest request, Configuration operationConfiguration = null);

/// Query public lobbies
/// </summary>
/// <param name="request">Request object for QueryLobbies</param>
/// <param name="operationConfiguration">Configuration for QueryLobbies</param>
/// <returns>Task for a Response object containing status code, headers, and QueryResponse object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<QueryResponse>> QueryLobbiesAsync(QueryLobbiesRequest request, Configuration operationConfiguration = null);

/// Query available lobbies and join a random one
/// </summary>
/// <param name="request">Request object for QuickJoinLobby</param>
/// <param name="operationConfiguration">Configuration for QuickJoinLobby</param>
/// <returns>Task for a Response object containing status code, headers, and Lobby object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<Lobby>> QuickJoinLobbyAsync(QuickJoinLobbyRequest request, Configuration operationConfiguration = null);

/// Remove a player
/// </summary>
/// <param name="request">Request object for RemovePlayer</param>
/// <param name="operationConfiguration">Configuration for RemovePlayer</param>
/// <returns>Task for a Response object containing status code, headers</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response> RemovePlayerAsync(RemovePlayerRequest request, Configuration operationConfiguration = null);

/// Update lobby data
/// </summary>
/// <param name="request">Request object for UpdateLobby</param>
/// <param name="operationConfiguration">Configuration for UpdateLobby</param>
/// <returns>Task for a Response object containing status code, headers, and Lobby object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<Lobby>> UpdateLobbyAsync(UpdateLobbyRequest request, Configuration operationConfiguration = null);

/// Update player data
/// </summary>
/// <param name="request">Request object for UpdatePlayer</param>
/// <param name="operationConfiguration">Configuration for UpdatePlayer</param>
/// <returns>Task for a Response object containing status code, headers, and Lobby object</returns>
/// <exception cref="Unity.Services.Lobbies.Http.HttpException">An exception containing the HttpClientResponse with headers, response code, and string of error.</exception>
Task<Response<Lobby>> UpdatePlayerAsync(UpdatePlayerRequest request, Configuration operationConfiguration = null);

}
public LobbyApiClient(IHttpClient httpClient,
TaskScheduler taskScheduler,
Configuration configuration = null) : base(httpClient, taskScheduler)
Configuration configuration = null) : base(httpClient)
{
// We don't need to worry about the configuration being null at
// this stage, we will check this in the accessor.

public async Task<Response<Lobby>> GetLobbyAsync(GetLobbyRequest request,
Configuration operationConfiguration = null)
{
var statusCodeToTypeMap = new Dictionary<string, System.Type>() { { "200", typeof(Lobby) },{ "403", typeof(ErrorStatus) },{ "404", typeof(ErrorStatus) } };
var statusCodeToTypeMap = new Dictionary<string, System.Type>() { { "200", typeof(Lobby) },{ "400", typeof(ErrorStatus) },{ "403", typeof(ErrorStatus) },{ "404", typeof(ErrorStatus) } };
// Merge the operation/request level configuration with the client level configuration.
var finalConfiguration = Configuration.MergeConfigurations(operationConfiguration, Configuration);

var handledResponse = ResponseHandler.HandleAsyncResponse<Lobby>(response, statusCodeToTypeMap);
return new Response<Lobby>(response, handledResponse);
}
public async Task<Response> HeartbeatAsync(HeartbeatRequest request,
Configuration operationConfiguration = null)
{
var statusCodeToTypeMap = new Dictionary<string, System.Type>() { { "204", null },{ "400", typeof(ErrorStatus) },{ "403", typeof(ErrorStatus) },{ "404", typeof(ErrorStatus) } };
// Merge the operation/request level configuration with the client level configuration.
var finalConfiguration = Configuration.MergeConfigurations(operationConfiguration, Configuration);
var response = await HttpClient.MakeRequestAsync("POST",
request.ConstructUrl(finalConfiguration.BasePath),
request.ConstructBody(),
request.ConstructHeaders(_accessToken, finalConfiguration),
finalConfiguration.RequestTimeout);
ResponseHandler.HandleAsyncResponse(response, statusCodeToTypeMap);
return new Response(response);
}
public async Task<Response<Lobby>> JoinLobbyByCodeAsync(JoinLobbyByCodeRequest request,

111
Packages/com.unity.services.lobby/Runtime/Apis/LobbyApiRequests.cs


using System.Collections.Generic;
using System.Linq;
using UnityEngine.Scripting;
using System.Text;
using System.Text.RegularExpressions;

key = UnityWebRequest.EscapeURL(key);
value = UnityWebRequest.EscapeURL(value);
queryParams.Add($"{key}={value}");
public List<string> AddParamsToQueryParams(List<string> queryParams, string key, List<string> values)
public List<string> AddParamsToQueryParams(List<string> queryParams, string key, List<string> values, string style, bool explode)
foreach(var value in values)
if (explode)
{
foreach(var value in values)
{
string escapedValue = UnityWebRequest.EscapeURL(value);
queryParams.Add($"{UnityWebRequest.EscapeURL(key)}={escapedValue}");
}
}
else
string escapedValue = UnityWebRequest.EscapeURL(value);
queryParams.Add($"{UnityWebRequest.EscapeURL(key)}[]={escapedValue}");
string paramString = $"{UnityWebRequest.EscapeURL(key)}=";
foreach(var value in values)
{
paramString += UnityWebRequest.EscapeURL(value) + ",";
}
paramString = paramString.Remove(paramString.Length - 1);
queryParams.Add(paramString);
return queryParams;
}

string[] accepts = {
"application/json",
"application/problem+json"
};
var acceptHeader = GenerateAcceptHeader(accepts);
if (!string.IsNullOrEmpty(acceptHeader))
{
headers.Add("Accept", acceptHeader);
}
var contentTypeHeader = GenerateContentTypeHeader(contentTypes);
if (!string.IsNullOrEmpty(contentTypeHeader))
{
headers.Add("Content-Type", contentTypeHeader);
}
// We also check if there are headers that are defined as part of
// the request configuration.
if (operationConfiguration != null && operationConfiguration.Headers != null)
{
foreach (var pair in operationConfiguration.Headers)
{
headers[pair.Key] = pair.Value;
}
}
return headers;
}
}
[Preserve]
public class HeartbeatRequest : LobbyApiBaseRequest
{
[Preserve]
public string LobbyId { get; }
[Preserve]
public object Body { get; }
string PathAndQueryParams;
/// <summary>
/// Heartbeat Request Object.
/// Heartbeat a lobby
/// </summary>
/// <param name="lobbyId">The id of the lobby to execute the request against.</param>
/// <param name="body">body param</param>
/// <returns>A Heartbeat request object.</returns>
[Preserve]
public HeartbeatRequest(string lobbyId, object body = default(object))
{
LobbyId = lobbyId;
Body = body;
PathAndQueryParams = $"/{lobbyId}/heartbeat";
List<string> queryParams = new List<string>();
if (queryParams.Count > 0)
{
PathAndQueryParams = $"{PathAndQueryParams}?{string.Join("&", queryParams)}";
}
}
public string ConstructUrl(string requestBasePath)
{
return requestBasePath + PathAndQueryParams;
}
public byte[] ConstructBody()
{
if(Body != null)
{
return ConstructBody(Body);
}
return null;
}
public Dictionary<string, string> ConstructHeaders(IAccessToken accessToken,
Configuration operationConfiguration = null)
{
var headers = new Dictionary<string, string>();
if(!string.IsNullOrEmpty(accessToken.AccessToken))
{
headers.Add("authorization", "Bearer " + accessToken.AccessToken);
}
string[] contentTypes = {
"application/json"
};
string[] accepts = {
"application/problem+json"
};

4
Packages/com.unity.services.lobby/Runtime/Http/BaseApiClient.cs


{
protected readonly IHttpClient HttpClient;
public BaseApiClient(IHttpClient httpClient, TaskScheduler scheduler)
public BaseApiClient(IHttpClient httpClient)
HttpClient = httpClient ?? new HttpClient(scheduler);
HttpClient = httpClient ?? new HttpClient();
}
}
}

14
Packages/com.unity.services.lobby/Runtime/Http/DeserializationException.cs


[Serializable]
public class DeserializationException : Exception
{
public HttpClientResponse response;
public DeserializationException() : base()
{
}

DeserializationException(string message, Exception inner) : base(message, inner)
{
}
public DeserializationException(HttpClientResponse httpClientResponse) : base(
"Unable to Deserialize Http Client Response")
{
response = httpClientResponse;
}
public DeserializationException(HttpClientResponse httpClientResponse, string message) : base(
message)
{
response = httpClientResponse;
}
}
}

54
Packages/com.unity.services.lobby/Runtime/Http/HttpClient.cs


};
private static readonly List<int> ErrorCodes = new List<int> {408, 500, 502, 503, 504};
private TaskScheduler _scheduler;
public HttpClient(TaskScheduler scheduler)
{
_scheduler = scheduler;
}
public void Get(string url, Dictionary<string, string> headers, Action<HttpClientResponse> onCompleted,
int requestTimeout = 10)
{
_scheduler.ScheduleMainThreadTask(() =>
{
_scheduler.StartCoroutine(ProcessRequest(UnityWebRequest.kHttpVerbGET, url, headers, null,
requestTimeout, onCompleted));
});
}
public void Delete(string url, Dictionary<string, string> headers, Action<HttpClientResponse> onCompleted,
int requestTimeout = 10)
{
_scheduler.ScheduleMainThreadTask(() =>
{
_scheduler.StartCoroutine(ProcessRequest(UnityWebRequest.kHttpVerbDELETE, url, headers, null,
requestTimeout, onCompleted));
});
}
public void Post(string url, byte[] body, Dictionary<string, string> headers,
Action<HttpClientResponse> onCompleted, int requestTimeout = 10)
{
_scheduler.ScheduleMainThreadTask(() =>
{
_scheduler.StartCoroutine(ProcessRequest(UnityWebRequest.kHttpVerbPOST, url, headers, body,
requestTimeout, onCompleted));
});
}
public void Put(string url, byte[] body, Dictionary<string, string> headers,
Action<HttpClientResponse> onCompleted, int requestTimeout = 10)
public HttpClient()
_scheduler.ScheduleMainThreadTask(() =>
{
_scheduler.StartCoroutine(ProcessRequest(UnityWebRequest.kHttpVerbPUT, url, headers, body,
requestTimeout, onCompleted));
});
}
public void MakeRequest(string method, string url, byte[] body, Dictionary<string, string> headers,
Action<HttpClientResponse> onCompleted, int requestTimeout = 10)
{
_scheduler.ScheduleMainThreadTask(() =>
{
_scheduler.StartCoroutine(ProcessRequest(method.ToUpper(), url, headers, body, requestTimeout,
onCompleted));
});
}
public async Task<HttpClientResponse> MakeRequestAsync(string method, string url, byte[] body,

13
Packages/com.unity.services.lobby/Runtime/Http/HttpException.cs


using System;
using UnityEngine.Scripting;
[Preserve]
[Preserve]
[Preserve]
[Preserve]
[Preserve]
[Preserve]
public HttpException(HttpClientResponse response) : base(response.ErrorMessage)
{
Response = response;

[Serializable]
[Preserve]
[Preserve]
[Preserve]
[Preserve]
[Preserve]
[Preserve]
public HttpException(HttpClientResponse response, T actualError) : base(response)
{
ActualError = actualError;

5
Packages/com.unity.services.lobby/Runtime/Http/IHttpClient.cs


{
public interface IHttpClient
{
void Get(string url, Dictionary<string, string> headers, Action<HttpClientResponse> onCompleted, int requestTimeout = 10);
void Delete(string url, Dictionary<string, string> headers, Action<HttpClientResponse> onCompleted, int requestTimeout = 10);
void Post(string url, byte[] body, Dictionary<string, string> headers, Action<HttpClientResponse> onCompleted, int requestTimeout = 10);
void Put(string url, byte[] body, Dictionary<string, string> headers, Action<HttpClientResponse> onCompleted, int requestTimeout = 10);
void MakeRequest(string method, string url, byte[] body, Dictionary<string, string> headers, Action<HttpClientResponse> onCompleted, int requestTimeout = 10);
Task<HttpClientResponse> MakeRequestAsync(string method, string url, byte[] body, Dictionary<string, string> headers, int requestTimeout);
Task<HttpClientResponse> MakeRequestAsync(string method, string url, List<IMultipartFormSection> body, Dictionary<string, string> headers, int requestTimeout, string boundary = null);
}

4
Packages/com.unity.services.lobby/Runtime/Http/JsonHelpers.cs


using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Utilities;
using UnityEngine;

success = false;
args.ErrorContext.Handled = true;
},
MissingMemberHandling = MissingMemberHandling.Ignore
MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Ignore
};
result = JsonConvert.DeserializeObject<T>(@this, settings);
return success;

58
Packages/com.unity.services.lobby/Runtime/Http/ResponseHandler.cs


{
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Ignore
MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Ignore
return JsonConvert.DeserializeObject<T>(GetDeserializardJson(response.Data), settings);
return JsonConvert.DeserializeObject<T>(GetDeserializedJson(response.Data), settings);
}
public static object TryDeserializeResponse(HttpClientResponse response, Type type)

MissingMemberHandling = MissingMemberHandling.Ignore
MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Ignore
return JsonConvert.DeserializeObject(GetDeserializardJson(response.Data), type, settings);
return JsonConvert.DeserializeObject(GetDeserializedJson(response.Data), type, settings);
private static string GetDeserializardJson(byte[] data)
private static string GetDeserializedJson(byte[] data)
{
return Encoding.UTF8.GetString(data);
}

}
catch (ArgumentException e)
{
throw new DeserializationException(response, e.Message);
throw new ResponseDeserializationException(response, e.Message);
throw new DeserializationException(response,
throw new ResponseDeserializationException(response,
catch (DeserializationException e) when (e.response == null)
catch (ResponseDeserializationException e)
throw new DeserializationException(response, e.Message);
}
catch (DeserializationException)
{
if (e.response == null)
{
throw new ResponseDeserializationException(response, e.Message);
}
throw new DeserializationException(response);
throw new ResponseDeserializationException(response);
}
}

}
catch (ArgumentException e)
{
throw new DeserializationException(response, e.Message);
throw new ResponseDeserializationException(response, e.Message);
throw new DeserializationException(response,
throw new ResponseDeserializationException(response,
catch (DeserializationException e) when (e.response == null)
catch (ResponseDeserializationException e)
throw new DeserializationException(response, e.Message);
}
catch (DeserializationException)
{
if (e.response == null)
{
throw new ResponseDeserializationException(response, e.Message);
}
throw new DeserializationException(response);
throw new ResponseDeserializationException(response);
}
}

}
catch (ArgumentException e)
{
throw new DeserializationException(response, e.Message);
throw new ResponseDeserializationException(response, e.Message);
throw new DeserializationException(response,
throw new ResponseDeserializationException(response,
catch (DeserializationException e) when (e.response == null)
{
throw new DeserializationException(response, e.Message);
}
catch (DeserializationException)
catch (ResponseDeserializationException e)
if (e.response == null)
{
throw new ResponseDeserializationException(response, e.Message);
}
throw new DeserializationException(response);
throw new ResponseDeserializationException(response);
}
}
}

5
Packages/com.unity.services.lobby/Runtime/LobbyServiceProvider.cs


public Task Initialize(CoreRegistry registry)
{
_gameObjectFactory = GameObjectFactory.CreateCoreSdkGameObject();
var scheduler = _gameObjectFactory.GetComponent<TaskScheduler>();
var httpClient = new HttpClient(scheduler);
var httpClient = new HttpClient();
LobbyService.LobbyApiClient = new LobbyApiClient(httpClient, scheduler, accessTokenLobbyApi);
LobbyService.LobbyApiClient = new LobbyApiClient(httpClient, accessTokenLobbyApi);
}
return Task.CompletedTask;

13
Packages/com.unity.services.lobby/Runtime/Models/CreateRequest.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// The body of a Create Lobby request.
/// <param name="name">The name of the lobby that should be displayed to users. All whitespace will be trimmed from name.</param>
/// <param name="maxPlayers">The maximum number of players allowed in the lobby.</param>
/// <param name="isPrivate">Indicates whether or not the lobby is publicly visible and will show up in query results. If the lobby is not publicly visible, the creator can share the &#x60;lobbyCode&#x60; with other users who can use it to join this lobby.</param>
/// <param name="player">player param</param>
/// <param name="data">Custom game-specific properties that apply to the lobby (e.g. &#x60;mapName&#x60; or &#x60;gameType&#x60;).</param>
[Preserve]
[DataContract(Name = "CreateRequest")]
public class CreateRequest

/// <param name="player">player param</param>
/// <param name="data">Custom game-specific properties that apply to the lobby (e.g. &#x60;mapName&#x60; or &#x60;gameType&#x60;).</param>
[Preserve]
public CreateRequest(string name, int maxPlayers, bool? isPrivate = null, Player player = default(Player), Dictionary<string, DataObject> data = null)
public CreateRequest(string name, int maxPlayers, bool? isPrivate = false, Player player = default, Dictionary<string, DataObject> data = default)
{
Name = name;
MaxPlayers = maxPlayers;

}
/// <summary>
/// The name of the lobby that should be displayed to users. All whitespace will be trimmed from name.
/// </summary>

[DataMember(Name = "isPrivate", EmitDefaultValue = true)]
public bool? IsPrivate{ get; }
/// <summary>
/// player param
/// </summary>
[Preserve]
[DataMember(Name = "player", EmitDefaultValue = false)]
public Player Player{ get; }

29
Packages/com.unity.services.lobby/Runtime/Models/DataObject.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// Custom data property for a lobby.
/// <param name="value">The value of the custom property. This property can be set to null or empty string. If this property is indexed (by setting the &#x60;index&#x60; field) then the length of the value must be less than 128 bytes.</param>
/// <param name="visibility">Indicates for whom the property should be visible. If &#x60;public&#x60;, the property will be visible to everyone and will be included in query results. If &#x60;member&#x60; the data will only be visible to users who are members of the lobby (i.e. those who have successfully joined). If &#x60;private&#x60;, the metadata will only be visible to the host.</param>
/// <param name="index">The name of the column to index this property value under, either &#x60;S#&#x60; for strings or &#x60;N#&#x60; for numeric values. If an index is specified on a property, then you can use that index name in a &#x60;QueryFilter&#x60; to filter results by that property. If You will not be prevented from indexing multiple objects having properties different with names but the same the same index, but you will likely receive unexpected results from a query.</param>
[Preserve]
[DataContract(Name = "DataObject")]
public class DataObject

/// <param name="value">The value of the custom property. This property can be set to null or empty string. If this property is indexed (by setting the &#x60;index&#x60; field) then the length of the value must be less than 128 bytes.</param>
/// <param name="index">The name of the column to index this property value under, either &#x60;S#&#x60; for strings or &#x60;N#&#x60; for numeric values. If an index is specified on a property, then you can use that index name in a &#x60;QueryFilter&#x60; to filter results by that property. If You will not be prevented from indexing multiple objects having properties different with names but the same the same index, but you will likely receive unexpected results from a query.</param>
[Preserve]
public DataObject(VisibilityOptions visibility, string value = null, IndexOptions index = default)
public DataObject(VisibilityOptions visibility, string value = default, IndexOptions index = default)
Value = value;
Value = value;
/// <summary>
/// The value of the custom property. This property can be set to null or empty string. If this property is indexed (by setting the &#x60;index&#x60; field) then the length of the value must be less than 128 bytes.
/// </summary>
[Preserve]
[DataMember(Name = "value", EmitDefaultValue = false)]
public string Value{ get; }
/// <summary>
/// Indicates for whom the property should be visible. If &#x60;public&#x60;, the property will be visible to everyone and will be included in query results. If &#x60;member&#x60; the data will only be visible to users who are members of the lobby (i.e. those who have successfully joined). If &#x60;private&#x60;, the metadata will only be visible to the host.
/// </summary>

public VisibilityOptions Visibility{ get; }
/// <summary>
/// The value of the custom property. This property can be set to null or empty string. If this property is indexed (by setting the &#x60;index&#x60; field) then the length of the value must be less than 128 bytes.
/// </summary>
[Preserve]
[DataMember(Name = "value", EmitDefaultValue = false)]
public string Value{ get; }
/// <summary>
/// The name of the column to index this property value under, either &#x60;S#&#x60; for strings or &#x60;N#&#x60; for numeric values. If an index is specified on a property, then you can use that index name in a &#x60;QueryFilter&#x60; to filter results by that property. If You will not be prevented from indexing multiple objects having properties different with names but the same the same index, but you will likely receive unexpected results from a query.
/// </summary>
[Preserve]

[Preserve]
[JsonConverter(typeof(StringEnumConverter))]
public enum VisibilityOptions
{

/// The name of the column to index this property value under, either &#x60;S#&#x60; for strings or &#x60;N#&#x60; for numeric values. If an index is specified on a property, then you can use that index name in a &#x60;QueryFilter&#x60; to filter results by that property. If You will not be prevented from indexing multiple objects having properties different with names but the same the same index, but you will likely receive unexpected results from a query.
/// </summary>
/// <value>The name of the column to index this property value under, either &#x60;S#&#x60; for strings or &#x60;N#&#x60; for numeric values. If an index is specified on a property, then you can use that index name in a &#x60;QueryFilter&#x60; to filter results by that property. If You will not be prevented from indexing multiple objects having properties different with names but the same the same index, but you will likely receive unexpected results from a query.</value>
[Preserve]
[JsonConverter(typeof(StringEnumConverter))]
public enum IndexOptions
{

}
}

13
Packages/com.unity.services.lobby/Runtime/Models/Detail.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// Additional detail about an error. This may include detailed validation failure messages, debugging information, troubleshooting steps, or more.
/// <param name="errorType">errorType param</param>
/// <param name="message">message param</param>
[Preserve]
[DataContract(Name = "Detail")]
public class Detail

/// <param name="errorType">errorType param</param>
/// <param name="message">message param</param>
[Preserve]
public Detail(string errorType = default(string), string message = default(string))
public Detail(string errorType = default, string message = default)
/// <summary>
/// errorType param
/// </summary>
/// <summary>
/// message param
/// </summary>
[Preserve]
[DataMember(Name = "message", EmitDefaultValue = false)]
public string Message{ get; }

17
Packages/com.unity.services.lobby/Runtime/Models/ErrorStatus.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// The body that will be returned for any failing request. We are using the [RFC 7807 Error Format](https://www.rfc-editor.org/rfc/rfc7807.html#section-3.1).
/// <param name="status">status param</param>
/// <param name="title">title param</param>
/// <param name="details">details param</param>
[Preserve]
[DataContract(Name = "ErrorStatus")]
public class ErrorStatus

/// <param name="title">title param</param>
/// <param name="details">details param</param>
[Preserve]
public ErrorStatus(int status = default(int), string title = default(string), List<Detail> details = default(List<Detail>))
public ErrorStatus(int status = default, string title = default, List<Detail> details = default)
{
Status = status;
Title = title;

/// <summary>
/// status param
/// </summary>
/// <summary>
/// title param
/// </summary>
/// <summary>
/// details param
/// </summary>
[Preserve]
[DataMember(Name = "details", EmitDefaultValue = false)]
public List<Detail> Details{ get; }

10
Packages/com.unity.services.lobby/Runtime/Models/JoinByCodeRequest.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// The body of a Join Lobby request using lobby code.
/// <param name="lobbyCode">The lobby code of the lobby the join. Mutually exclusive with &#x60;id&#x60;. This is used to join a private lobby where the lobby code was shared to other users manually.</param>
/// <param name="player">player param</param>
[Preserve]
[DataContract(Name = "JoinByCodeRequest")]
public class JoinByCodeRequest

/// <param name="lobbyCode">The lobby code of the lobby the join. Mutually exclusive with &#x60;id&#x60;. This is used to join a private lobby where the lobby code was shared to other users manually.</param>
/// <param name="player">player param</param>
[Preserve]
public JoinByCodeRequest(string lobbyCode, Player player = default(Player))
public JoinByCodeRequest(string lobbyCode, Player player = default)
/// <summary>
/// The lobby code of the lobby the join. Mutually exclusive with &#x60;id&#x60;. This is used to join a private lobby where the lobby code was shared to other users manually.
/// </summary>

/// <summary>
/// player param
/// </summary>
[Preserve]
[DataMember(Name = "player", EmitDefaultValue = false)]
public Player Player{ get; }

20
Packages/com.unity.services.lobby/Runtime/Models/Lobby.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// Data about an individual lobby.
/// <param name="id">id param</param>
/// <param name="lobbyCode">A short code that be used to join a lobby. This is only visible to lobby members. Typically this is displayed to the user so they can share it with other players out of game. Users with the code can join the lobby even when it is private.</param>
/// <param name="upid">The Unity project ID of the game.</param>
/// <param name="name">The name of the lobby. Typically this shown in game UI to represent the lobby.</param>
/// <param name="maxPlayers">The maximum number of players that can be members of the lobby.</param>
/// <param name="availableSlots">The number of remaining open slots for players before the lobby becomes full.</param>
/// <param name="isPrivate">Whether the lobby is private or not. Private lobbies do not appear in query results.</param>
/// <param name="players">The members of the lobby.</param>
/// <param name="data">Properties of the lobby set by the host.</param>
/// <param name="hostId">The ID of the player that is the lobby host.</param>
/// <param name="created">When the lobby was created. The timestamp is in UTC and conforms to ISO 8601.</param>
/// <param name="lastUpdated">When the lobby was last updated. The timestamp is in UTC and conforms to ISO 8601.</param>
[Preserve]
[DataContract(Name = "Lobby")]
public class Lobby

/// <param name="created">When the lobby was created. The timestamp is in UTC and conforms to ISO 8601.</param>
/// <param name="lastUpdated">When the lobby was last updated. The timestamp is in UTC and conforms to ISO 8601.</param>
[Preserve]
public Lobby(string id = default(string), string lobbyCode = null, string upid = default(string), string name = null, int maxPlayers = default(int), int availableSlots = default(int), bool isPrivate = default(bool), List<Player> players = default(List<Player>), Dictionary<string, DataObject> data = null, string hostId = default(string), DateTime created = default(DateTime), DateTime lastUpdated = default(DateTime))
public Lobby(string id = default, string lobbyCode = default, string upid = default, string name = default, int maxPlayers = default, int availableSlots = default, bool isPrivate = default, List<Player> players = default, Dictionary<string, DataObject> data = default, string hostId = default, DateTime created = default, DateTime lastUpdated = default)
{
Id = id;
LobbyCode = lobbyCode;

LastUpdated = lastUpdated;
}
/// <summary>
/// id param
/// </summary>
[Preserve]
[DataMember(Name = "id", EmitDefaultValue = false)]
public string Id{ get; }

11
Packages/com.unity.services.lobby/Runtime/Models/Player.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// Information about a specific player creating, joining, or already in a lobby.
/// <param name="id">The unique identifier for the player. If not provided for a create or join request it will be set to the id of the caller.</param>
/// <param name="connectionInfo">(TBD) Connection information for connecting to a relay with this player.</param>
/// <param name="data">Custom game-specific properties that apply to an individual player (e.g. &#x60;role&#x60; or &#x60;skill&#x60;).</param>
/// <param name="allocationId">An id that associates this player in this lobby with a persistent connection. When a disconnect notification is recevied, this value is used to identify the associated player in a lobby to mark them as disconnected.</param>
/// <param name="joined">The time at which the player joined the lobby.</param>
/// <param name="lastUpdated">The last time the metadata for this player was updated.</param>
[Preserve]
[DataContract(Name = "Player")]
public class Player

/// <param name="joined">The time at which the player joined the lobby.</param>
/// <param name="lastUpdated">The last time the metadata for this player was updated.</param>
[Preserve]
public Player(string id = null, string connectionInfo = null, Dictionary<string, PlayerDataObject> data = null, string allocationId = null, DateTime joined = default(DateTime), DateTime lastUpdated = default(DateTime))
public Player(string id = default, string connectionInfo = default, Dictionary<string, PlayerDataObject> data = default, string allocationId = default, DateTime joined = default, DateTime lastUpdated = default)
{
Id = id;
ConnectionInfo = connectionInfo;

LastUpdated = lastUpdated;
}
/// <summary>
/// The unique identifier for the player. If not provided for a create or join request it will be set to the id of the caller.
/// </summary>

24
Packages/com.unity.services.lobby/Runtime/Models/PlayerDataObject.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// Custom data property for a player.
/// <param name="value">The value of the custom property. This property can be set to null or empty string.</param>
/// <param name="visibility">Indicates for whom the property should be visible. If &#x60;public&#x60;, the property will be visible to everyone and will be included in query results. If &#x60;member&#x60; the data will only be visible to users who are members of the lobby (i.e. those who have successfully joined). If &#x60;private&#x60;, the metadata will only be visible to the the player.</param>
[Preserve]
[DataContract(Name = "PlayerDataObject")]
public class PlayerDataObject

/// <param name="visibility">Indicates for whom the property should be visible. If &#x60;public&#x60;, the property will be visible to everyone and will be included in query results. If &#x60;member&#x60; the data will only be visible to users who are members of the lobby (i.e. those who have successfully joined). If &#x60;private&#x60;, the metadata will only be visible to the the player.</param>
/// <param name="value">The value of the custom property. This property can be set to null or empty string.</param>
[Preserve]
public PlayerDataObject(VisibilityOptions visibility, string value = null)
public PlayerDataObject(VisibilityOptions visibility, string value = default)
Visibility = visibility;
Visibility = visibility;
/// Indicates for whom the property should be visible. If &#x60;public&#x60;, the property will be visible to everyone and will be included in query results. If &#x60;member&#x60; the data will only be visible to users who are members of the lobby (i.e. those who have successfully joined). If &#x60;private&#x60;, the metadata will only be visible to the the player.
/// The value of the custom property. This property can be set to null or empty string.
[JsonConverter(typeof(StringEnumConverter))]
[DataMember(Name = "visibility", IsRequired = true, EmitDefaultValue = true)]
public VisibilityOptions Visibility{ get; }
[DataMember(Name = "value", EmitDefaultValue = false)]
public string Value{ get; }
/// The value of the custom property. This property can be set to null or empty string.
/// Indicates for whom the property should be visible. If &#x60;public&#x60;, the property will be visible to everyone and will be included in query results. If &#x60;member&#x60; the data will only be visible to users who are members of the lobby (i.e. those who have successfully joined). If &#x60;private&#x60;, the metadata will only be visible to the the player.
[DataMember(Name = "value", EmitDefaultValue = false)]
public string Value{ get; }
[JsonConverter(typeof(StringEnumConverter))]
[DataMember(Name = "visibility", IsRequired = true, EmitDefaultValue = true)]
public VisibilityOptions Visibility{ get; }
/// <summary>

[Preserve]
[JsonConverter(typeof(StringEnumConverter))]
public enum VisibilityOptions
{

13
Packages/com.unity.services.lobby/Runtime/Models/PlayerUpdateRequest.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// The body of an Update Player Data request.
/// <param name="connectionInfo">(TBD) Connection information for connecting to a relay with this player.</param>
/// <param name="data">Custom game-specific properties to add, update, or remove from the player (e.g. &#x60;role&#x60; or &#x60;skill&#x60;). To remove an existing property, include it in &#x60;data&#x60; but set the property object to &#x60;null&#x60;. To update the value to &#x60;null&#x60;, set the &#x60;value&#x60; property of the object to &#x60;null&#x60;.</param>
/// <param name="allocationId">An id that associates this player in this lobby with a persistent connection. When a disconnect notification is recevied, this value is used to identify the associated player in a lobby to mark them as disconnected.</param>
[Preserve]
[DataContract(Name = "PlayerUpdateRequest")]
public class PlayerUpdateRequest

/// <param name="data">Custom game-specific properties to add, update, or remove from the player (e.g. &#x60;role&#x60; or &#x60;skill&#x60;). To remove an existing property, include it in &#x60;data&#x60; but set the property object to &#x60;null&#x60;. To update the value to &#x60;null&#x60;, set the &#x60;value&#x60; property of the object to &#x60;null&#x60;.</param>
/// <param name="allocationId">An id that associates this player in this lobby with a persistent connection. When a disconnect notification is recevied, this value is used to identify the associated player in a lobby to mark them as disconnected.</param>
[Preserve]
public PlayerUpdateRequest(string connectionInfo = null, Dictionary<string, PlayerDataObject> data = null, string allocationId = null)
public PlayerUpdateRequest(string connectionInfo = default, Dictionary<string, PlayerDataObject> data = default, string allocationId = default)
Data = data;
Data = new JsonObject(data);
/// <summary>
/// (TBD) Connection information for connecting to a relay with this player.
/// </summary>

/// Custom game-specific properties to add, update, or remove from the player (e.g. &#x60;role&#x60; or &#x60;skill&#x60;). To remove an existing property, include it in &#x60;data&#x60; but set the property object to &#x60;null&#x60;. To update the value to &#x60;null&#x60;, set the &#x60;value&#x60; property of the object to &#x60;null&#x60;.
/// </summary>
[Preserve]
[JsonConverter(typeof(JsonObjectConverter))]
public Dictionary<string, PlayerDataObject> Data{ get; }
public JsonObject Data{ get; }
/// <summary>
/// An id that associates this player in this lobby with a persistent connection. When a disconnect notification is recevied, this value is used to identify the associated player in a lobby to mark them as disconnected.

8
Packages/com.unity.services.lobby/Runtime/Models/QueryFilter.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// A filter for an individual field that is applied to a query.
/// <param name="field">The name of the field to filter on. For custom data fields, the name of the index must be used instead of the field name.</param>
/// <param name="value">The value to compare to the field being filtered. This value must be a string and it must be parsable as the same type as &#x60;field&#x60; (e.g. &#x60;integer&#x60; for MaxPlayers, &#x60;datetime&#x60; for Created, etc.). The value for &#x60;datetime&#x60; fields (Created, LastUpdated) must be in RFC3339 format. For example, in C# this can be achieved using the \&quot;o\&quot; format specifier: &#x60;return dateTime.ToString(\&quot;o\&quot;, DateTimeFormatInfo.InvariantInfo);&#x60;. Refer to your language documentation for other methods to generate RFC3339-compatible datetime strings.</param>
/// <param name="op">The operator used to compare the field to the filter value. Supports &#x60;CONTAINS&#x60; (only on the &#x60;Name&#x60; field), &#x60;EQ&#x60; (Equal), &#x60;NE&#x60; (Not Equal), &#x60;LT&#x60; (Less Than), &#x60;LE&#x60; (Less Than or Equal), &#x60;GT&#x60; (Greater Than), or &#x60;GE&#x60; (Greater Than or Equal).</param>
[Preserve]
[DataContract(Name = "QueryFilter")]
public class QueryFilter

Op = op;
}
/// <summary>
/// The name of the field to filter on. For custom data fields, the name of the index must be used instead of the field name.
/// </summary>

/// The name of the field to filter on. For custom data fields, the name of the index must be used instead of the field name.
/// </summary>
/// <value>The name of the field to filter on. For custom data fields, the name of the index must be used instead of the field name.</value>
[Preserve]
[JsonConverter(typeof(StringEnumConverter))]
public enum FieldOptions
{

/// The operator used to compare the field to the filter value. Supports &#x60;CONTAINS&#x60; (only on the &#x60;Name&#x60; field), &#x60;EQ&#x60; (Equal), &#x60;NE&#x60; (Not Equal), &#x60;LT&#x60; (Less Than), &#x60;LE&#x60; (Less Than or Equal), &#x60;GT&#x60; (Greater Than), or &#x60;GE&#x60; (Greater Than or Equal).
/// </summary>
/// <value>The operator used to compare the field to the filter value. Supports &#x60;CONTAINS&#x60; (only on the &#x60;Name&#x60; field), &#x60;EQ&#x60; (Equal), &#x60;NE&#x60; (Not Equal), &#x60;LT&#x60; (Less Than), &#x60;LE&#x60; (Less Than or Equal), &#x60;GT&#x60; (Greater Than), or &#x60;GE&#x60; (Greater Than or Equal).</value>
[Preserve]
[JsonConverter(typeof(StringEnumConverter))]
public enum OpOptions
{

8
Packages/com.unity.services.lobby/Runtime/Models/QueryOrder.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// An order for an individual field that is applied to a query.
/// <param name="asc">Whether to sort in ascending or descending order.</param>
/// <param name="field">The name of the field to order on.</param>
[Preserve]
[DataContract(Name = "QueryOrder")]
public class QueryOrder

/// <param name="asc">Whether to sort in ascending or descending order.</param>
/// <param name="field">The name of the field to order on.</param>
[Preserve]
public QueryOrder(bool asc = default(bool), FieldOptions field = default)
public QueryOrder(bool asc = default, FieldOptions field = default)
/// <summary>
/// Whether to sort in ascending or descending order.
/// </summary>

/// The name of the field to order on.
/// </summary>
/// <value>The name of the field to order on.</value>
[Preserve]
[JsonConverter(typeof(StringEnumConverter))]
public enum FieldOptions
{

11
Packages/com.unity.services.lobby/Runtime/Models/QueryRequest.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// The body of a Query request which defines how to sort and filter results, how many results to return, etc.
/// <param name="count">The number of results to return.</param>
/// <param name="skip">The number of results to skip before selecting results to return.</param>
/// <param name="sampleResults">Whether a random sample of results that match the search filter should be returned.</param>
/// <param name="filter">A list of filters which can be used to narrow down which lobbies to return.</param>
/// <param name="order">A list of orders which define how the results should be ordered in the response.</param>
/// <param name="continuationToken">A continuation token that can be passed to subsequent query requests to fetch the next page of results.</param>
[Preserve]
[DataContract(Name = "QueryRequest")]
public class QueryRequest

/// <param name="order">A list of orders which define how the results should be ordered in the response.</param>
/// <param name="continuationToken">A continuation token that can be passed to subsequent query requests to fetch the next page of results.</param>
[Preserve]
public QueryRequest(int? count = null, int? skip = null, bool sampleResults = false, List<QueryFilter> filter = default(List<QueryFilter>), List<QueryOrder> order = default(List<QueryOrder>), string continuationToken = null)
public QueryRequest(int? count = 10, int? skip = 0, bool sampleResults = false, List<QueryFilter> filter = default, List<QueryOrder> order = default, string continuationToken = default)
{
Count = count;
Skip = skip;

ContinuationToken = continuationToken;
}
/// <summary>
/// The number of results to return.
/// </summary>

13
Packages/com.unity.services.lobby/Runtime/Models/QueryResponse.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// A list of lobbies that matched the specified lobbies query. Only the public top level, data, and player data properties are returned.
/// <param name="results">results param</param>
/// <param name="continuationToken">continuationToken param</param>
[Preserve]
[DataContract(Name = "QueryResponse")]
public class QueryResponse

/// <param name="results">results param</param>
/// <param name="continuationToken">continuationToken param</param>
[Preserve]
public QueryResponse(List<Lobby> results = default(List<Lobby>), string continuationToken = null)
public QueryResponse(List<Lobby> results = default, string continuationToken = default)
/// <summary>
/// results param
/// </summary>
/// <summary>
/// continuationToken param
/// </summary>
[Preserve]
[DataMember(Name = "continuationToken", EmitDefaultValue = false)]
public string ContinuationToken{ get; }

10
Packages/com.unity.services.lobby/Runtime/Models/QuickJoinRequest.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// The body of a QuickJoin request.
/// <param name="filter">A list of filters which can be used to narrow down which lobbies to attempt to join..</param>
/// <param name="player">player param</param>
[Preserve]
[DataContract(Name = "QuickJoinRequest")]
public class QuickJoinRequest

/// <param name="filter">A list of filters which can be used to narrow down which lobbies to attempt to join..</param>
/// <param name="player">player param</param>
[Preserve]
public QuickJoinRequest(List<QueryFilter> filter = default(List<QueryFilter>), Player player = default(Player))
public QuickJoinRequest(List<QueryFilter> filter = default, Player player = default)
/// <summary>
/// A list of filters which can be used to narrow down which lobbies to attempt to join..
/// </summary>

/// <summary>
/// player param
/// </summary>
[Preserve]
[DataMember(Name = "player", EmitDefaultValue = false)]
public Player Player{ get; }

15
Packages/com.unity.services.lobby/Runtime/Models/UpdateRequest.cs


using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Unity.Services.Lobbies.Http;

/// The body of an Update Lobby request.
/// <param name="name">The name of the lobby that should be displayed to users. All whitespace will be trimmed from name.</param>
/// <param name="maxPlayers">The maximum number of players allowed in the lobby. Must be greater than or equal to the current number of players in the lobby.</param>
/// <param name="isPrivate">Indicates whether or not the lobby is publicly visible and will show up in query results. If the lobby is not publicly visible, the creator can share the &#x60;lobbyCode&#x60; with other users who can use it to join this lobby.</param>
/// <param name="data">Custom game-specific properties to add, update, or remove from the lobby (e.g. &#x60;mapName&#x60; or &#x60;gameType&#x60;). To remove an existing property, include it in &#x60;data&#x60; but set the property object to &#x60;null&#x60;. To update the value to &#x60;null&#x60;, set the &#x60;value&#x60; property of the object to &#x60;null&#x60;.</param>
/// <param name="hostId">The id of the player to make the host of the lobby. As soon as this is updated the current host will no longer have permission to modify the lobby.</param>
[Preserve]
[DataContract(Name = "UpdateRequest")]
public class UpdateRequest

/// <param name="data">Custom game-specific properties to add, update, or remove from the lobby (e.g. &#x60;mapName&#x60; or &#x60;gameType&#x60;). To remove an existing property, include it in &#x60;data&#x60; but set the property object to &#x60;null&#x60;. To update the value to &#x60;null&#x60;, set the &#x60;value&#x60; property of the object to &#x60;null&#x60;.</param>
/// <param name="hostId">The id of the player to make the host of the lobby. As soon as this is updated the current host will no longer have permission to modify the lobby.</param>
[Preserve]
public UpdateRequest(string name = null, int? maxPlayers = null, bool? isPrivate = null, Dictionary<string, DataObject> data = null, string hostId = null)
public UpdateRequest(string name = default, int? maxPlayers = default, bool? isPrivate = default, Dictionary<string, DataObject> data = default, string hostId = default)
Data = data;
Data = new JsonObject(data);
/// <summary>
/// The name of the lobby that should be displayed to users. All whitespace will be trimmed from name.
/// </summary>

/// Custom game-specific properties to add, update, or remove from the lobby (e.g. &#x60;mapName&#x60; or &#x60;gameType&#x60;). To remove an existing property, include it in &#x60;data&#x60; but set the property object to &#x60;null&#x60;. To update the value to &#x60;null&#x60;, set the &#x60;value&#x60; property of the object to &#x60;null&#x60;.
/// </summary>
[Preserve]
[JsonConverter(typeof(JsonObjectConverter))]
public Dictionary<string, DataObject> Data{ get; }
public JsonObject Data{ get; }
/// <summary>
/// The id of the player to make the host of the lobby. As soon as this is updated the current host will no longer have permission to modify the lobby.

4
Packages/com.unity.services.lobby/Runtime/Scheduler/ThreadHelper.cs


using System.Threading;
using UnityEditor;
using UnityEngine;
namespace Unity.Services.Lobbies.Scheduler

private static System.Threading.Tasks.TaskScheduler _taskScheduler;
private static int _mainThreadId;
#if UNITY_EDITOR
[InitializeOnLoadMethod]
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void Init()
{

44
Packages/com.unity.services.lobby/package.json


{
"name": "com.unity.services.lobby",
"displayName": "Unity Lobby",
"version": "0.2.0-preview",
"unity": "2020.3",
"description": "Client API SDK Package for communicating with the Unity Lobby service",
"dependencies": {
"com.unity.services.core": "1.1.0-pre.2",
"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"
},
"relatedPackages": {
"com.unity.services.lobby.tests": "0.2.0-preview"
},
"upmCi": {
"footprint": "aa7999c19cdbcfe8d97e68dde6d16c73ef3411c1"
},
"repository": {
"url": "https://github.cds.internal.unity3d.com/unity/com.unity.services.rooms.git",
"type": "git",
"revision": "902923a105886dde15661bfeb44db49ee0fdbaff"
},
"samples": [
{
"displayName": "Hello World Sample",
"description": "This sample walks through using most of the Lobby APIs in a simple, linear way.",
"path": "Samples~/HelloWorld"
"name": "com.unity.services.lobby",
"displayName":"Unity Lobby",
"version": "0.2.1-preview",
"unity": "2020.3",
"description": "Client API SDK Package for communicating with the Unity Lobby service",
"dependencies": {
"com.unity.services.core": "1.1.0-pre.2",
"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"
]
}

9
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.ide.rider": "3.0.7",
"com.unity.ide.visualstudio": "2.0.9",

"com.unity.test-framework": "1.1.26",
"com.unity.textmeshpro": "3.0.6",
"com.unity.timeline": "1.6.0-pre.5",
"com.unity.toolchain.win-x86_64-linux-x86_64": "0.1.20-preview",
"com.unity.ugui": "1.0.0",
"com.unity.modules.ai": "1.0.0",

"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0"
}
}
}

90
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",
"depth": 1,
"version": "1.5.3",
"depth": 2,
"source": "registry",
"dependencies": {
"com.unity.mathematics": "1.2.1"

"source": "registry",
"dependencies": {
"com.unity.ugui": "1.0.0"
},
"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"
},

4
ProjectSettings/ProjectVersion.txt


m_EditorVersion: 2021.2.0b1
m_EditorVersionWithRevision: 2021.2.0b1 (b0978dae4864)
m_EditorVersion: 2020.3.14f1
m_EditorVersionWithRevision: 2020.3.14f1 (d0d1bb862f9d)

2
Packages/com.unity.services.lobby/Runtime/Http/DeserializationSettings.cs.meta


fileFormatVersion: 2
guid: 36b1cdc3d10416a44ac98753d781b1c5
guid: c7097123bafddd84a882a4ef0b85a9f3
MonoImporter:
externalObjects: {}
serializedVersion: 2

17
Assets/Scripts/Entities/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/Entities/EmoteType.cs.meta


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

229
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 will handle observing the local player and updating remote players over Relay when there are local changes.
/// 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;
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);
}
protected virtual void Uninitialize()
{
m_localUser.onChanged -= OnLocalChange;
Leave();
Locator.Get.UpdateSlow.Unsubscribe(UpdateSlow);
}
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;
m_networkDriver.ScheduleUpdate().Complete();
foreach (NetworkConnection conn in m_connections)
DoUserUpdate(m_networkDriver, conn, m_localUser);
}
public 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()
{
m_networkDriver.ScheduleUpdate().Complete(); // This pumps all messages, which pings the Relay allocation and keeps it alive.
ReceiveNetworkEvents(m_networkDriver, m_connections);
if (!m_hasSentInitialMessage)
SendInitialMessage(m_networkDriver, m_connections[0]);
}
private void ReceiveNetworkEvents(NetworkDriver driver, List<NetworkConnection> connections)
{
DataStreamReader strm;
NetworkEvent.Type cmd;
foreach (NetworkConnection connection in connections)
{
while ((cmd = connection.PopEvent(driver, out strm)) != NetworkEvent.Type.Empty)
{
ProcessNetworkEvent(connection, strm, cmd);
}
}
}
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)) // TODO: Do we want to hold onto the message if the user isn't present *now* in case they're pending?
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);
}
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);
}
private void SendInitialMessage(NetworkDriver driver, NetworkConnection connection)
{
ForceFullUserUpdate(driver, connection, m_localUser); // Assuming this is only created after the Relay connection is successful.
m_hasSentInitialMessage = true;
}
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);
}
protected void ForceFullUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user)
{
// TODO: Write full state in one message?
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 given all up-to-date info.
/// </summary>
private void OnNewConnection(NetworkConnection conn)
{
// When a new client connects, they need to be updated with the current state of everyone else.
// (We can't exclude this client from the events we send to it, since we don't have its ID in strm, but it will ignore messages about itself on arrival.)
foreach (var user in m_localLobby.LobbyUsers)
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);
}
}
// 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)
{
// TODO: If a client disconnects, see if remaining players are all already ready.
}
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()
{
m_networkDriver.ScheduleUpdate().Complete();
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();
if (!conn.IsCreated) // "Nothing more to accept" is signalled by returning an invalid connection from Accept.
break;
m_connections.Add(conn);
OnNewConnection(conn);
}
}
}
}

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


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

207
Assets/Scripts/Relay/RelayUtpSetup.cs


using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Jobs;
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 JobHandle m_currentUpdateHandle;
protected LocalLobby m_localLobby;
protected LobbyUser m_localUser;
protected Action<bool, RelayUtpClient> m_onJoinComplete;
public enum MsgType { NewPlayer = 0, Ping = 1, ReadyState = 2, PlayerName = 3, Emote = 4, StartCountdown = 5, CancelCountdown = 6, ConfirmInGame = 7, EndInGame = 8 }
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();
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()); // TODO: This is the only reason for being a MonoBehaviour?
}
private IEnumerator WaitForBindComplete()
{
while (!m_networkDriver.Bound)
{
m_networkDriver.ScheduleUpdate().Complete();
yield return null; // TODO: Does this not proceed until a client connects as well?
}
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
}
public class RelayUtpSetupHost : RelayUtpSetup
{
[Flags]
private enum JoinState { None = 0, Bound = 1, Joined = 2 }
private JoinState m_joinState = JoinState.None;
protected override void JoinRelay()
{
RelayInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnAllocation);
}
private void OnAllocation(Allocation allocation)
{
RelayInterface.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;
RelayInterface.JoinAsync(m_localLobby.RelayCode, OnJoin);
}
private void OnJoin(JoinAllocation joinAllocation)
{
m_localLobby.RelayServer = new ServerAddress(joinAllocation.RelayServer.IpV4, joinAllocation.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);
}
}
}
public class RelayUtpSetupClient : RelayUtpSetup
{
protected override void JoinRelay()
{
m_localLobby.onChanged += OnLobbyChange;
}
private void OnLobbyChange(LocalLobby lobby)
{
if (m_localLobby.RelayCode != null)
{
RelayInterface.JoinAsync(m_localLobby.RelayCode, OnJoin);
m_localLobby.onChanged -= OnLobbyChange;
}
}
private void OnJoin(JoinAllocation allocation)
{
if (allocation == null)
return;
BindToAllocation(allocation.RelayServer.IpV4, allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.ConnectionData, allocation.HostConnectionData, allocation.Key, 1);
m_localLobby.RelayServer = new ServerAddress(allocation.RelayServer.IpV4, allocation.RelayServer.Port);
}
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
{
RelayUtpClient watcher = gameObject.AddComponent<RelayUtpClient>();
watcher.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, watcher);
}
}
}
}

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


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

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:

9
Packages/com.unity.services.lobby/CONTRIBUTING.md


# Contributing
## If you are interested in contributing, here are some ground rules:
* ... Define guidelines & rules for what contributors need to know to successfully make Pull requests against your repo ...
## All contributions are subject to the [Unity Contribution Agreement(UCA)](https://unity3d.com/legal/licenses/Unity_Contribution_Agreement)
By making a pull request, you are confirming agreement to the terms and conditions of the UCA, including that your Contributions are your original creation and that you have complete right and authority to make your Contributions.
## Once you have a change ready following these ground rules. Simply make a pull request

7
Packages/com.unity.services.lobby/CONTRIBUTING.md.meta


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

15
Packages/com.unity.services.lobby/Runtime/Http/DeserializationSettings.cs


using System;
namespace Unity.Services.Lobbies.Http
{
public enum MissingMemberHandling
{
Error,
Ignore
}
public class DeserializationSettings
{
public MissingMemberHandling MissingMemberHandling = MissingMemberHandling.Error;
}
}

53
Packages/com.unity.services.lobby/Runtime/Http/JsonObject.cs


using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Utilities;
using UnityEngine;
namespace Unity.Services.Lobbies.Http
{
public class JsonObject
{
internal JsonObject(object obj)
{
this.obj = obj;
}
internal object obj;
public string GetAsString()
{
try
{
return JsonConvert.SerializeObject(obj);
}
catch (System.Exception e)
{
throw new System.Exception("Failed to convert JsonObject to string.");
}
}
public T GetAs<T>(DeserializationSettings deserializationSettings = null)
{
// Check if derializationSettings is null so we can use the default value.
deserializationSettings = deserializationSettings ?? new DeserializationSettings();
JsonSerializerSettings jsonSettings = new JsonSerializerSettings
{
MissingMemberHandling = deserializationSettings.MissingMemberHandling == MissingMemberHandling.Error
? Newtonsoft.Json.MissingMemberHandling.Error
: Newtonsoft.Json.MissingMemberHandling.Ignore
};
try
{
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj), jsonSettings);
}
catch (Newtonsoft.Json.JsonSerializationException e)
{
throw new DeserializationException(e.Message);
}
catch (System.Exception e)
{
throw new DeserializationException("Unable to deserialize object.");
}
}
}
}

11
Packages/com.unity.services.lobby/Runtime/Http/JsonObject.cs.meta


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

28
Packages/com.unity.services.lobby/Runtime/Http/JsonObjectConverter.cs


using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Utilities;
using UnityEngine;
namespace Unity.Services.Lobbies.Http
{
class JsonObjectConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JsonObject jobj = (JsonObject) value;
JToken t = JToken.FromObject(jobj.obj);
t.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer)
{
throw new System.NotImplementedException();
}
public override bool CanConvert(System.Type objectType)
{
throw new System.NotImplementedException();
}
}
}

11
Packages/com.unity.services.lobby/Runtime/Http/JsonObjectConverter.cs.meta


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

34
Packages/com.unity.services.lobby/Runtime/Http/ResponseDeserializationException.cs


using System;
namespace Unity.Services.Lobbies.Http
{
[Serializable]
public class ResponseDeserializationException : Exception
{
public HttpClientResponse response;
public ResponseDeserializationException() : base()
{
}
public ResponseDeserializationException(string message) : base(message)
{
}
ResponseDeserializationException(string message, Exception inner) : base(message, inner)
{
}
public ResponseDeserializationException(HttpClientResponse httpClientResponse) : base(
"Unable to Deserialize Http Client Response")
{
response = httpClientResponse;
}
public ResponseDeserializationException(HttpClientResponse httpClientResponse, string message) : base(
message)
{
response = httpClientResponse;
}
}
}

11
Packages/com.unity.services.lobby/Runtime/Http/ResponseDeserializationException.cs.meta


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

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": ""
}
}

273
Assets/Prefabs/UI/CountDownUI.prefab


%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &5694388898566309151
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2473738857440996537}
- component: {fileID: 9125082141416656856}
- component: {fileID: 288221731430065532}
- component: {fileID: 1604604243888226905}
- component: {fileID: 1102405501498744344}
- component: {fileID: 3702758058023689559}
m_Layer: 5
m_Name: CountDownUI
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2473738857440996537
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5694388898566309151}
m_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: 2508362878409625140}
m_Father: {fileID: 0}
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: -77.436}
m_SizeDelta: {x: -50, y: -404.7342}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9125082141416656856
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5694388898566309151}
m_CullTransparentMesh: 1
--- !u!114 &288221731430065532
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5694388898566309151}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5b3b588e7ae40ec4ca35fdb9404513ab, type: 3}
m_Name:
m_EditorClassIdentifier:
m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_CountDownText: {fileID: 6871324529924300226}
--- !u!114 &1604604243888226905
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5694388898566309151}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f38cf340acfcd4c64a6968b7386ad570, type: 3}
m_Name:
m_EditorClassIdentifier:
m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
m_ShowThisWhen: 2
--- !u!114 &1102405501498744344
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5694388898566309151}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
OnObservedUpdated:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 288221731430065532}
m_TargetAssemblyTypeName: CountdownUI, LobbyRooms
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_Target: {fileID: 1604604243888226905}
m_TargetAssemblyTypeName: LobbyRooms.UI.LobbyStateVisibilityUI, LobbyRooms
m_MethodName: ObservedUpdated
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 0
--- !u!225 &3702758058023689559
CanvasGroup:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5694388898566309151}
m_Enabled: 1
m_Alpha: 1
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!1 &6120000553027287259
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2508362878409625140}
- component: {fileID: 3642688559417843241}
- component: {fileID: 6871324529924300226}
m_Layer: 5
m_Name: CountdownText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2508362878409625140
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6120000553027287259}
m_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: 2473738857440996537}
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: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3642688559417843241
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6120000553027287259}
m_CullTransparentMesh: 1
--- !u!114 &6871324529924300226
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6120000553027287259}
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: 'Starting in:'
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: 21
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 21
m_fontStyle: 0
m_HorizontalAlignment: 1
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: 5, y: 5, z: 5, 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}

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


fileFormatVersion: 2
guid: f90e4035352c7ce40b68e50109a9bb4f
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/Lobby/ReadyCheck.cs.meta


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

63
Assets/Scripts/Lobby/ReadyCheck.cs


using System;
using System.Collections.Generic;
using System.Linq;
namespace LobbyRelaySample
{
/// <summary>
/// On the host, this will watch for all players to ready, and once they have, it will prepare for a synchronized countdown.
/// </summary>
public class ReadyCheck : IDisposable
{
float m_ReadyTime = 5;
public ReadyCheck(float readyTime = 5)
{
m_ReadyTime = readyTime;
}
public void BeginCheckingForReady()
{
Locator.Get.UpdateSlow.Subscribe(OnUpdate);
}
public void EndCheckingForReady()
{
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate);
}
/// <summary>
/// Checks the lobby to see if we have all Readied up. If so, send out a message with the target time at which to end a countdown.
/// </summary>
void OnUpdate(float dt)
{
var lobby = LobbyAsyncRequests.Instance.CurrentLobby;
if (lobby == null || lobby.Players.Count == 0)
return;
int readyCount = lobby.Players.Count((p) =>
{
if (p.Data?.ContainsKey("UserStatus") != true) // Needs to be "!= true" to handle null properly.
return false;
UserStatus status;
if (Enum.TryParse(p.Data["UserStatus"].Value, out status))
return status == UserStatus.Ready;
return false;
});
if (readyCount == lobby.Players.Count)
{
Dictionary<string, string> data = new Dictionary<string, string>();
DateTime targetTime = DateTime.Now.AddSeconds(m_ReadyTime);
data.Add("AllPlayersReady", targetTime.Ticks.ToString());
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(data, null);
EndCheckingForReady();
}
}
public void Dispose()
{
EndCheckingForReady();
}
}
}

/Packages/com.unity.services.lobby/Samples~/HelloWorld/LobbyHelloWorld.cs.meta → /Packages/com.unity.services.lobby/Runtime/Http/DeserializationSettings.cs.meta

正在加载...
取消
保存