当前提交
db92ac88
共有 321 个文件被更改,包括 10609 次插入 和 1335 次删除
-
19Assets/Prefabs/UI/CreateContent.prefab
-
161Assets/Prefabs/UI/GameCanvas.prefab
-
893Assets/Prefabs/UI/JoinContent.prefab
-
91Assets/Prefabs/UI/JoinCreateCanvas.prefab
-
32Assets/Prefabs/UI/LobbyButtonUI.prefab
-
14Assets/Prefabs/UI/LobbyCodeCanvas.prefab
-
560Assets/Prefabs/UI/LobbyGameCanvas.prefab
-
130Assets/Prefabs/UI/LobbyUserList.prefab
-
868Assets/Prefabs/UI/PlayerInteractionPanel.prefab
-
30Assets/Prefabs/UI/RelayCodeCanvas.prefab
-
19Assets/Prefabs/UI/RenamePopup.prefab
-
174Assets/Prefabs/UI/UserCardPanel.prefab
-
2Assets/Prefabs/GameManager.prefab
-
767Assets/Scenes/mainScene.unity
-
12Assets/Scripts/Auth/Identity.cs
-
6Assets/Scripts/Auth/NameGenerator.cs
-
4Assets/Scripts/Auth/SubIdentity_Authentication.cs
-
48Assets/Scripts/Infrastructure/Locator.cs
-
48Assets/Scripts/Infrastructure/LogHandler.cs
-
84Assets/Scripts/Infrastructure/Messenger.cs
-
7Assets/Scripts/Infrastructure/Observed.cs
-
9Assets/Scripts/Infrastructure/ObserverBehaviour.cs
-
163Assets/Scripts/Infrastructure/UpdateSlow.cs
-
118Assets/Scripts/Lobby/LobbyAPIInterface.cs
-
248Assets/Scripts/Lobby/LobbyAsyncRequests.cs
-
56Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
-
67Assets/Scripts/Lobby/ToLocalLobby.cs
-
5Assets/Scripts/LobbyRelaySample.asmdef
-
3Assets/Scripts/Tests/Editor/AuthTests.cs
-
38Assets/Scripts/Tests/Editor/LoggerTests.cs
-
53Assets/Scripts/Tests/Editor/MessengerTests.cs
-
22Assets/Scripts/Tests/Editor/ObserverTests.cs
-
80Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs
-
99Assets/Scripts/Tests/PlayMode/RelayRoundTripTests.cs
-
112Assets/Scripts/Tests/PlayMode/UpdateSlowTests.cs
-
12Assets/Scripts/UI/CountdownUI.cs
-
4Assets/Scripts/UI/EmoteButtonUI.cs
-
2Assets/Scripts/UI/EndGameButtonUI.cs
-
19Assets/Scripts/UI/InLobbyUserUI.cs
-
1Assets/Scripts/UI/JoinCreateLobbyUI.cs
-
24Assets/Scripts/UI/JoinMenuUI.cs
-
2Assets/Scripts/UI/ReadyCheckUI.cs
-
15Assets/Scripts/UI/SpinnerUI.cs
-
24Assets/Scripts/UI/UIPanelBase.cs
-
5Assets/Scripts/Game/ServerAddress.cs
-
136Assets/Scripts/Game/LocalLobby.cs
-
15Assets/Scripts/Game/LocalGameState.cs
-
124Assets/Scripts/Game/LobbyUser.cs
-
14Assets/Scripts/Game/LobbyServiceData.cs
-
19Packages/manifest.json
-
164Packages/packages-lock.json
-
22ProjectSettings/PackageManagerSettings.asset
-
20ProjectSettings/ProjectSettings.asset
-
4ProjectSettings/ProjectVersion.txt
-
2ProjectSettings/UnityConnectSettings.asset
-
157README.md
-
2Assets/Scripts/UI/RateLimitVisibility.cs.meta
-
8Assets/Art/Crown.meta
-
1001Assets/Prefabs/UI/PopUpUI.prefab
-
7Assets/Prefabs/UI/PopUpUI.prefab.meta
-
44Assets/Scripts/Infrastructure/AsyncRequest.cs
-
11Assets/Scripts/Infrastructure/AsyncRequest.cs.meta
-
32Assets/Scripts/Infrastructure/LogHandlerSettings.cs
-
11Assets/Scripts/Infrastructure/LogHandlerSettings.cs.meta
-
66Assets/Scripts/Relay/RelayAPIInterface.cs
-
243Assets/Scripts/Relay/RelayUtpClient.cs
-
11Assets/Scripts/Relay/RelayUtpClient.cs.meta
-
146Assets/Scripts/Relay/RelayUtpHost.cs
-
11Assets/Scripts/Relay/RelayUtpHost.cs.meta
-
225Assets/Scripts/Relay/RelayUtpSetup.cs
-
11Assets/Scripts/Relay/RelayUtpSetup.cs.meta
-
25Assets/Scripts/UI/PopUpUI.cs
-
11Assets/Scripts/UI/PopUpUI.cs.meta
-
35Assets/Scripts/UI/RateLimitVisibility.cs
-
39Assets/Scripts/UI/RecolorForLobbyType.cs
-
11Assets/Scripts/UI/RecolorForLobbyType.cs.meta
-
17Assets/Scripts/Game/EmoteType.cs
-
11Assets/Scripts/Game/EmoteType.cs.meta
-
331Assets/Scripts/Game/GameManager.cs
-
16ProjectSettings/BurstAotSettings_StandaloneWindows.json
-
6ProjectSettings/CommonBurstAotSettings.json
-
1001README.pdf
-
1001~Documentation/Images/1_lobby_list.PNG
-
1001~Documentation/Images/2_lobby.PNG
-
119Assets/Art/Crown/Crown.png
-
96Assets/Art/Crown/Crown.png.meta
-
90Packages/com.unity.transport/.gitattributes
-
3Packages/com.unity.transport/.gitmodules
-
250Packages/com.unity.transport/CHANGELOG.md
-
7Packages/com.unity.transport/CHANGELOG.md.meta
-
20Packages/com.unity.transport/DESIGN.md
-
7Packages/com.unity.transport/DESIGN.md.meta
-
20Packages/com.unity.transport/Documentation~/TableOfContents.md
-
17Packages/com.unity.transport/Documentation~/connection-state-machine.md
-
60Packages/com.unity.transport/Documentation~/event-consumption.md
-
94Packages/com.unity.transport/Documentation~/images/Pipeline-stages-diagram.png
893
Assets/Prefabs/UI/JoinContent.prefab
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
868
Assets/Prefabs/UI/PlayerInteractionPanel.prefab
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
767
Assets/Scenes/mainScene.unity
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
|
|||
m_EditorVersion: 2021.2.0b1 |
|||
m_EditorVersionWithRevision: 2021.2.0b1 (b0978dae4864) |
|||
m_EditorVersion: 2020.3.15f2 |
|||
m_EditorVersionWithRevision: 2020.3.15f2 (6cf78cb77498) |
|
|||
# Game Lobby Sample |
|||
|
|||
_Tested with Unity 2020.3 for PC and Mac._ |
|||
### Closed Beta - 7/14/21 |
|||
Lobby and Relay are **only** available in closed beta at the moment. To use these services, you will need to have signed up here for the services to show in your organization: https://create.unity3d.com/relay-lobby-beta-signup |
|||
This sample demonstrates how to use the Lobby and Relay packages to create a typical game lobby experience. Players can host lobbies that other players can join using a public lobby list or lobby code, and then connect with Relay to use Unity Transport ("UTP") for basic real-time communication between them. Relay allows players to securely communicate with each other while maintaining connection anonymity. |
|||
|
|||
|
|||
**Note**: This is not a “drag-and-drop” solution; the Game Lobby Sample is not a minimal code sample intended to be completely copied into a full-scale project. Rather, it demonstrates how to use multiple services in a vertical slice with some basic game logic and infrastructure. Use it as a reference and starting point to learn how Lobby and Relay work together and how to integrate them into your project. |
|||
|
|||
|
|||
#### Features: |
|||
|
|||
|
|||
* **Anonymous Auth login**: Track player credentials without a persistent account. |
|||
* **Lobby creation**: Players host lobbies for others to join. |
|||
* **Lobby query**: Find a list of lobbies with filters, or use lobby codes. |
|||
* **Relay obfuscation**: Players in a lobby are connected through an anonymous IP. |
|||
* **UTP communication**: Players transmit basic data to lobby members in real time. |
|||
* **Lobby + Relay connection management**: Together, the services automatically handle new connections and disconnections. |
|||
# Game Lobby Sample |
|||
## *Unity 2021.2 0b1* |
|||
|
|||
### Service organization setup |
|||
|
|||
To use Unity’s multiplayer services, you need a cloud organization ID for your project. If you do not currently have one, follow the **How do I create a new Organization?** article to set up your cloud organization: |
|||
|
|||
[https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-](https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-) |
|||
|
|||
Once you have an ID, link it to your project under **Edit **>** Project Settings **>** Services** and use the Unity Dashboard to manage your project’s services. |
|||
|
|||
|
|||
### Service overview |
|||
|
|||
|
|||
#### **Lobby** |
|||
|
|||
The Lobby service allows developers to create lobbies and share data between players before a real-time network connection is established. It simplifies the first step in connecting users to other services such as Relay and provides tools to allow players to find other lobbies. |
|||
|
|||
The Lobby documentation contains code samples and additional information about the service. It includes comprehensive details for using Lobby along with additional code samples, and it might help you better understand the Game Lobby Sample: [http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service) |
|||
|
|||
The Lobby service can be managed in the Unity Dashboard: |
|||
This is a Unity project sample showing how to integrate Lobby and Relay into a typical game lobby experience. |
|||
[https://dashboard.unity3d.com/lobby](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service) |
|||
Features Covered: |
|||
- Lobby Creation |
|||
- Lobby Query |
|||
- Lobby Data Sync |
|||
- Emotes |
|||
- Player Names |
|||
- Player Ready Check State |
|||
- Lobby Join |
|||
- Relay Server Creation |
|||
- Relay Code Generation |
|||
- Relay Server Join |
|||
## Service Organization Setup |
|||
**Create an organization** |
|||
#### **Relay** |
|||
Follow the guide to set up your cloud organization: |
|||
The Relay service connects players in a host-client model with an obfuscated host IP. This allows players to host networked experiences as though players connected directly while only sharing private information with Relay itself. \ |
|||
[Organization Tutorial](https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-) |
|||
Then, in the Unity Editor, open Services > General Settings to create a cloud project ID (or link to an existing one) to associate the Unity project with your organization. |
|||
The Relay documentation contains code samples and additional information about the service. It includes comprehensive details for using Relay along with additional code samples, and it might help you better understand the Game Lobby Sample: |
|||
[http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview) |
|||
## Lobby & Relay |
|||
The Relay service can be managed in the Unity Dashboard: |
|||
We use the Lobby service to create a space that our users can join and share data through. |
|||
[https://dashboard.unity3d.com/relay](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview) |
|||
[Lobby Overview](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service) |
|||
In this sample, once players are connected to a lobby, they are connected through Relay to set up real-time data transfer over UTP. Lobby and Relay both depend on Auth for credentials. This sample uses Auth’s anonymous login feature to create semi-permanent credentials that are unique to each player but do not require developers to maintain a persistent account for them. |
|||
[Lobby Dashboard](https://dashboard.unity3d.com/lobby) |
|||
#### **Setup** |
|||
The Lobby and Relay sections of the Unity Dashboard contain their own setup instructions. Select **About & Support **>** Get Started** and follow the provided steps to integrate the services into your project. |
|||
We use the Relay service to obfuscate the hosts' IP, while still allowing them to locally host strangers. |
|||
With those services set up and your project linked to your cloud organization, open the **mainScene** scene in the Editor and begin using the Game Lobby Sample. |
|||
[Relay Overview](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview) |
|||
[Relay Dashboard]( https://dashboard.unity3d.com/relay) |
|||
### Running the sample |
|||
You will need two “players” to demonstrate the full sample functionality. Create a standalone build to run alongside the Editor in Play mode. Although Auth creates anonymous credentials using your machine’s registry, your Editor and your build have different credentials because they create different registry entries. |
|||
### Setup |
|||
For either one, select "About & Support => Get Started." |
|||
**Closed Beta Only** |
|||
#### **Lobby Join Menu** |
|||
Follow the steps, downloading your packaged folders to the Sample Project Package\Packages |
|||
![alt_text](~Documents/1_lobby_list.PNG "Lobby List") |
|||
*If you open the project and you get the "Enter Safe Mode" dialogue, it means you are missing your packages.* |
|||
The Lobby Join menu contains the lobby list UI, which acts as a hub for players to connect to each other using the public lobby list or lobby code. |
|||
*If you still cannot find the package namespaces, ensure the Assets/Scripts/LobbyRelaySample.asmdef is referencing the packages.* |
|||
Follow the steps until you reach "Lobby/Relay On." |
|||
1. **Public lobby list**: Shows all lobbies not set to private. Lobbies contain developer-defined data which can be set to public and non-public visibility. The Lobby service cleans up any “zombie” rooms so they don’t appear in this list. For this sample, lobby names and player counts are shown, and lobbies in the “in-game” state are not shown. You can select a lobby and then select **Join**. |
|||
2. **Refresh icon**: Refreshes the Lobby List. The Lobby service imposes rate limits on all API calls to prevent spamming. Refresh attempts within the rate limit will do nothing (approximately every 1.5 seconds, see [Lobby documentation](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service) for details). |
|||
3. **Lobby Code field**: Enter a lobby code for an existing lobby. In addition to the public lobby list, all lobbies can be joined using their codes. This allows players to privately share access to lobbies. |
|||
4. **Filters**: Sets the Lobby List to only show servers of a certain color. The Lobby service can filter any queries by data set to public visibility. For this sample, players can optionally filter by color, which hosts set for their lobbies. |
|||
5. **Join**:** **Requests to join by public lobby list selection or lobby code. Failed requests are also rate limited to prevent spam, if the player presses the button repeatedly. |
|||
6. **Create**: Allows creation of a new lobby. Players select a lobby name and whether to make a private lobby, and they then connect to the new lobby as its host. |
|||
7. **Player name**: Displays the player name and allows renaming. By default, players are assigned a name based on their anonymous Auth credentials, but name changes follow their credentials so that all players see the new name. |
|||
## Solo Testing |
|||
Create a build of the project in the OS of your choice. |
|||
The Authentication service creates a unique ID for builds, so you may run a build and the Editor at the same time to represent two users. |
|||
#### **Lobby View** |
|||
1. Enter Play mode, and select Start to open the lobby list. This queries the Lobby service for available lobbies, but there are currently none. |
|||
![alt_text](~Documents/2_lobby.PNG "Lobby") |
|||
![Join Menu](~Documentation/Images/tutorial_1_lobbyList.png?raw=true "Join Menu") |
|||
2. The Create menu lets you host a new lobby. |
|||
The Lobby View UI displays information from Lobby and Relay for all players in a lobby. When a new player joins, they immediately begin connecting to the host, after which they synchronize emotes and state changes with the other lobby members. |
|||
![Create Menu](~Documentation/Images/tutorial_2_createMenu.png?raw=true) |
|||
3. This is the lobby. It has a shareable lobby code to allow other users to join directly. |
|||
For demonstration purposes, we also show the Relay code, which will be passed to all users in the lobby. |
|||
![Lobby View](~Documentation/Images/tutorial_3_HostGame.png?raw=true) |
|||
1. **Lobby name**: Set when the lobby was created and cannot be changed. |
|||
2. **Lobby Code**: Shareable code generated by the Lobby service. This may be provided externally to other players to allow them to join this lobby. |
|||
3. **Lobby user**: A player in the lobby. The player’s name, state, and emote are displayed; this data is synchronized through Relay + UTP, so any changes that a player makes will appear immediately for all connected players. Incoming players will be sent the current data once they have connected. |
|||
4. **Relay IP**:** **The anonymous server IP that Relay generates. This does not need to be shown to players and is displayed here simply to indicate that Relay is functioning. |
|||
5. **Relay Code**: An internal join code generated by Relay that is used during Relay connection. This does not need to be shown to players and is displayed here simply to indicate that Relay is functioning. |
|||
6. **Emote icons**: Sets the player’s emote and is synchronized using UTP. |
|||
7. **Lobby color**: (Host only) Sets the lobby color for filtering in the Lobby List. This is synchronized through Lobby, so changes won’t appear immediately for all players because Lobby queries are rate limited. See Rate Limits. |
|||
8. **Ready button**: Sets a ready state on the player. When all players are ready, the host initiates a countdown to an “in-game” state, and the lobby becomes hidden from the public lobby list. |
|||
4. Run your build, and as this second user, you should now see your lobby in the list. |
|||
### Architecture |
|||
![Populated Join View](~Documentation/Images/tutorial_4_newLobby.png?raw=true) |
|||
The Game Lobby Sample is designed as a vertical slice of a multiplayer lobby, so it has additional infrastructure that might be expected in full game production, as well as some components to allow multiple services to work together. As such, not all of the codebase will be relevant depending on your needs. Most contents are self-documenting, but some high-level points follow: |
|||
5. The lobby holds up to 4 users and will pass the Relay code once all the users are ready. Changes to a user's name or emote will appear for other users after a couple seconds. |
|||
![Relay Ready!](~Documentation/Images/tutorial_5_editorCow.png?raw=true) |
|||
* Logic for using the Auth, Lobby, and Relay services are encapsulated in their own directories. Much of the API usage is abstracted away for convenience. |
|||
* For example, **LobbyAPIInterface** contains the actual calls into the Lobby API, but **LobbyAsyncRequests** has additional processing on the results of those calls as well as some structures necessary for timing the API calls properly. |
|||
* The Relay directory also contains logic for using the Unity Transport (UTP) because it requires a transport to function. |
|||
* The **Game** directory contains core “glue” classes for running the sample itself, representing a simple framework for a game. |
|||
* **GameManager** has all the core logic for running the sample. It sets up the services and UI, manages game states, and fields messages from other components. |
|||
* Various other classes exist here to maintain the states of multiple components of the sample and to interface between our sample’s needs for Lobby and Relay data and the structure of that data remotely in the services. |
|||
* The **Infrastructure** directory contains classes used for essential tasks related to overall function but not specifically to any service. |
|||
* **Locator **mimics a Service Locator pattern, allowing for behaviors that might otherwise be Singletons to easily be swapped in and out. |
|||
* **Messenger **creates a simple messaging system used to keep unrelated classes decoupled, letting them instead message arbitrary listeners when interesting things happen. For example, a lobby is updated, or a player disconnects from a relay. |
|||
* An** Observer** pattern is used for all UI elements and for local copies of remote Lobby and Relay data. An Observer is alerted whenever its observed data changes, and the owner of that data doesn’t need to know who is observing. |
|||
* The **UI **directory strictly contains logic for the sample’s UI and observing relevant data. Viewing these files should not be necessary to understand how to use the services themselves, though they do demonstrate the use of the Observer pattern. |
|||
* Several files exist with classes that simply implement **ObserverBehaviour**. This is because Unity requires **MonoBehaviours** to exist in files of the same names. |
|||
* Note that much of the UI is driven by **CanvasGroup** alpha for visibility, which means that some behaviors continue to run even when invisible to the player. |
|||
* Multiple **Tests** directories are included to demonstrate core behavior and edge cases for some of the code. In particular, the Play mode tests for Lobby and Relay can be used to ensure your connection to the services is functioning correctly. |
|||
* In the Editor, the project assets are broken into nested prefabs for convenience when making changes during sample development. Their details should not be considered vital, although there are UI elements that depend on event handlers that are serialized. |
|||
6. Once the lobby host has received a ready signal from all users, it will send out a countdown, and all users will enter a simultaneous countdown before connecting to Relay. |
|||
### Considerations |
|||
![Countdown!](~Documentation/Images/tutorial_6_countDown.png?raw=true) |
|||
While the Game Lobby Sample represents more than just a minimal implementation of the Lobby and Relay services, it is not comprehensive and some design decisions were made for faster or more readable development. |
|||
7. An anonymous IP from the Relay service is passed to all users in the lobby, at which point your game logic could connect them to a server and begin transmitting realtime data. |
|||
![InGame!](~Documentation/Images/tutorial_7_ingame.png?raw=true) |
|||
* All operations using Lobby and Relay rely on asynchronous API calls. The sample code has some logic for handling issues that can result from receiving the results at arbitrary times, but it doesn’t have logic for enqueuing calls that the user initiates during setup and cleanup. Rapid operations when entering and exiting lobbies can result in unexpected behavior. |
|||
* Relay does not support host migration, but Lobby does. If a lobby host disconnects, the lobby might seem to clients to continue to operate until Lobby detects the disconnect. In practice, you might want to implement an additional periodic handshake between hosts and clients in cases where data could get out of sync quickly. |
|||
* The sample sets up heartbeat pings with Lobby and Relay to keep the connections alive, but they do not impose any limitations on a lobby’s duration. Consider a maximum duration in actual use, such as a maximum game length. |
|||
* HTTP errors will appear in the console. These are returned by Lobby and Relay API calls for various reasons. In general, they do not impact the sample’s execution, though they might result in unexpected behavior for a player since the sample doesn’t provide any explanatory UI when these errors occur. |
|||
* **List of HTTP errors:** |
|||
* **404 (“Not Found”) errors** might occur when the Lobby service handles multiple incoming API calls in an arbitrary order, usually when leaving a lobby. They will also occur if trying to join an invalid lobby, such as one that has been deleted but still appears in the lobby list before refreshing. |
|||
* **429 (“Too Many Requests”) errors** occur if rate limited operations happen too quickly. In particular, refreshing the lobby list too quickly results in 429 errors from the **QueryLobbiesAsync** call. Consult the Lobby documentation for details. |
|||
* **401 (“Unauthorized”) errors** occur if the user enters the lobby menu before Auth sign-in completes, since all Lobby and Relay operations require Auth credentials. |
|||
* **409 (“Conflict”) errors** occur if a player tries to join a lobby using the same credentials as another player. In particular, this will happen if you are trying to test with multiple standalone builds, since they share the same registry entry on your machine. To test with three or more players on one machine: |
|||
* Create a duplicate project with Symbolic Links to the original **Assets **and **Packages**, so that it uses the same assets. Copy the **ProjectSettings** as well, but do not link them to the original. Note that the process for creating Symbolic Links will depend on your operating system. |
|||
* Open this project in a second Editor. |
|||
* Under **Edit **>** Project Settings **>** Player**, modify the **Product Name**. This causes the duplicate project to have a new registry entry, so Auth will assign new credentials. |
|||
* Verify that running the sample in either Play mode or a standalone build assigns a different default player name than the original. This indicates different Auth credentials, preventing the 409 errors. |
|
|||
fileFormatVersion: 2 |
|||
guid: 7fe20b638f61a144fb7e22bf8bd26bb6 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
1001
Assets/Prefabs/UI/PopUpUI.prefab
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
|
|||
fileFormatVersion: 2 |
|||
guid: 79d6084439b78bb4eaf5232cb953fd87 |
|||
PrefabImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Both Lobby and Relay have need for asynchronous requests with some basic safety wrappers. This is a shared place for that.
|
|||
/// </summary>
|
|||
public static class AsyncRequest |
|||
{ |
|||
public static async void DoRequest(Task task, Action onComplete) |
|||
{ |
|||
string currentTrace = System.Environment.StackTrace; // For debugging. If we don't get the calling context here, it's lost once the async operation begins.
|
|||
try |
|||
{ await task; |
|||
} |
|||
catch (Exception e) |
|||
{ Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e); |
|||
throw eFull; |
|||
} |
|||
finally |
|||
{ onComplete?.Invoke(); |
|||
} |
|||
} |
|||
public static async void DoRequest<T>(Task<T> task, Action<T> onComplete) |
|||
{ |
|||
T result = default; |
|||
string currentTrace = System.Environment.StackTrace; |
|||
try |
|||
{ result = await task; |
|||
} |
|||
catch (Exception e) |
|||
{ Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e); |
|||
throw eFull; |
|||
} |
|||
finally |
|||
{ onComplete?.Invoke(result); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 255a690fe68a18c438e160a118cd8a6b |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
using UnityEngine.Serialization; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public class LogHandlerSettings : MonoBehaviour |
|||
{ |
|||
[SerializeField] |
|||
[Tooltip("Only logs of this level or higher will appear in the console.")] |
|||
private LogMode m_editorLogVerbosity = LogMode.Critical; |
|||
|
|||
[SerializeField] |
|||
private PopUpUI m_popUpPrefab; |
|||
|
|||
[SerializeField] |
|||
private ErrorReaction m_errorReaction; |
|||
|
|||
void Awake() |
|||
{ |
|||
LogHandler.Get().mode = m_editorLogVerbosity; |
|||
LogHandler.Get().SetLogReactions(m_errorReaction); |
|||
} |
|||
|
|||
public void SpawnErrorPopup(string errorMessage) |
|||
{ |
|||
var popupInstance = Instantiate(m_popUpPrefab, transform); |
|||
popupInstance.ShowPopup(errorMessage, Color.red); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 089231d71bcfb8d479b4f8b778b1026f |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Relay; |
|||
using Unity.Services.Relay.Models; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.relay |
|||
{ |
|||
/// <summary>
|
|||
/// Wrapper for all the interaction with the Relay API.
|
|||
/// </summary>
|
|||
public static class RelayAPIInterface |
|||
{ |
|||
/// <summary>
|
|||
/// A Relay Allocation represents a "server" for a new host.
|
|||
/// </summary>
|
|||
public static async void AllocateAsync(int maxConnections, Action<Allocation> onComplete) |
|||
{ |
|||
try |
|||
{ |
|||
Allocation allocation = await Relay.Instance.CreateAllocationAsync(maxConnections); |
|||
|
|||
onComplete.Invoke(allocation); |
|||
} |
|||
catch (RelayServiceException ex) |
|||
{ |
|||
Debug.LogError($"Relay AllocateAsync returned a relay exception: {ex.Reason} - {ex.Message}"); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Only after an Allocation has been completed can a Relay join code be obtained. This code will be stored in the lobby's data as non-public
|
|||
/// such that players can retrieve the Relay join code only after connecting to the lobby.
|
|||
/// </summary>
|
|||
public static async void GetJoinCodeAsync(Guid hostAllocationId, Action<string> onComplete) |
|||
{ |
|||
try |
|||
{ |
|||
string joinCode = await Relay.Instance.GetJoinCodeAsync(hostAllocationId); |
|||
onComplete.Invoke(joinCode); |
|||
} |
|||
catch (RelayServiceException ex) |
|||
{ |
|||
Debug.LogError($"Relay GetJoinCodeAsync returned a relay exception: {ex.Reason} - {ex.Message}"); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clients call this to retrieve the host's Allocation via a Relay join code.
|
|||
/// </summary>
|
|||
public static async void JoinAsync(string joinCode, Action<JoinAllocation> onComplete) |
|||
{ |
|||
try |
|||
{ |
|||
JoinAllocation joinAllocation = await Relay.Instance.JoinAllocationAsync(joinCode); |
|||
onComplete.Invoke(joinAllocation); |
|||
} |
|||
catch (RelayServiceException ex) |
|||
{ |
|||
Debug.LogError($"Relay JoinCodeAsync returned a relay exception: {ex.Reason} - {ex.Message}"); |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using System.Collections.Generic; |
|||
using Unity.Networking.Transport; |
|||
using UnityEngine; |
|||
using MsgType = LobbyRelaySample.relay.RelayUtpSetup.MsgType; |
|||
|
|||
namespace LobbyRelaySample.relay |
|||
{ |
|||
/// <summary>
|
|||
/// This observes the local player and updates remote players over Relay when there are local changes, demonstrating basic data transfer over the Unity Transport (UTP).
|
|||
/// Created after the connection to Relay has been confirmed.
|
|||
/// </summary>
|
|||
public class RelayUtpClient : MonoBehaviour // This is a MonoBehaviour merely to have access to Update.
|
|||
{ |
|||
protected LobbyUser m_localUser; |
|||
protected LocalLobby m_localLobby; |
|||
protected NetworkDriver m_networkDriver; |
|||
protected List<NetworkConnection> m_connections; // For clients, this has just one member, but for hosts it will have more.
|
|||
|
|||
protected bool m_hasSentInitialMessage = false; |
|||
private const float k_heartbeatPeriod = 5; |
|||
|
|||
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby) |
|||
{ |
|||
m_localUser = localUser; |
|||
m_localLobby = localLobby; |
|||
m_localUser.onChanged += OnLocalChange; |
|||
m_networkDriver = networkDriver; |
|||
m_connections = connections; |
|||
Locator.Get.UpdateSlow.Subscribe(UpdateSlow, k_heartbeatPeriod); |
|||
} |
|||
protected virtual void Uninitialize() |
|||
{ |
|||
m_localUser.onChanged -= OnLocalChange; |
|||
Leave(); |
|||
Locator.Get.UpdateSlow.Unsubscribe(UpdateSlow); |
|||
m_networkDriver.Dispose(); |
|||
} |
|||
public void OnDestroy() |
|||
{ |
|||
Uninitialize(); |
|||
} |
|||
|
|||
private void OnLocalChange(LobbyUser localUser) |
|||
{ |
|||
if (m_connections.Count == 0) // This could be the case for the host alone in the lobby.
|
|||
return; |
|||
foreach (NetworkConnection conn in m_connections) |
|||
DoUserUpdate(m_networkDriver, conn, m_localUser); |
|||
} |
|||
|
|||
private void Update() |
|||
{ |
|||
OnUpdate(); |
|||
} |
|||
|
|||
private void UpdateSlow(float dt) |
|||
{ |
|||
// Clients need to send any data over UTP periodically, or else the connection will timeout.
|
|||
foreach (NetworkConnection connection in m_connections) |
|||
WriteByte(m_networkDriver, connection, "0", MsgType.Ping, 0); // The ID doesn't matter here, so send a minimal number of bytes.
|
|||
} |
|||
|
|||
protected virtual void OnUpdate() |
|||
{ |
|||
if (!m_hasSentInitialMessage) |
|||
ReceiveNetworkEvents(m_networkDriver); // Just on the first execution, make sure to handle any events that accumulated while completing the connection.
|
|||
m_networkDriver.ScheduleUpdate().Complete(); // This pumps all messages, which pings the Relay allocation and keeps it alive. It should be called no more often than ReceiveNetworkEvents.
|
|||
ReceiveNetworkEvents(m_networkDriver); // This reads the message queue which was just updated.
|
|||
if (!m_hasSentInitialMessage) |
|||
SendInitialMessage(m_networkDriver, m_connections[0]); // On a client, the 0th (and only) connection is to the host.
|
|||
} |
|||
|
|||
private void ReceiveNetworkEvents(NetworkDriver driver) |
|||
{ |
|||
NetworkConnection conn; |
|||
DataStreamReader strm; |
|||
NetworkEvent.Type cmd; |
|||
while ((cmd = driver.PopEvent(out conn, out strm)) != NetworkEvent.Type.Empty) // NetworkConnection also has PopEvent, but NetworkDriver.PopEvent automatically includes new connections.
|
|||
{ |
|||
ProcessNetworkEvent(conn, strm, cmd); |
|||
} |
|||
} |
|||
|
|||
// See the Write* methods for the expected event format.
|
|||
private void ProcessNetworkEvent(NetworkConnection conn, DataStreamReader strm, NetworkEvent.Type cmd) |
|||
{ |
|||
if (cmd == NetworkEvent.Type.Data) |
|||
{ |
|||
MsgType msgType = (MsgType)strm.ReadByte(); |
|||
string id = ReadLengthAndString(ref strm); |
|||
if (id == m_localUser.ID || !m_localLobby.LobbyUsers.ContainsKey(id)) // We don't hold onto messages, since an incoming user will be fully initialized before they send events.
|
|||
return; |
|||
|
|||
if (msgType == MsgType.PlayerName) |
|||
{ |
|||
string name = ReadLengthAndString(ref strm); |
|||
m_localLobby.LobbyUsers[id].DisplayName = name; |
|||
} |
|||
else if (msgType == MsgType.Emote) |
|||
{ |
|||
EmoteType emote = (EmoteType)strm.ReadByte(); |
|||
m_localLobby.LobbyUsers[id].Emote = emote; |
|||
} |
|||
else if (msgType == MsgType.ReadyState) |
|||
{ |
|||
UserStatus status = (UserStatus)strm.ReadByte(); |
|||
m_localLobby.LobbyUsers[id].UserStatus = status; |
|||
} |
|||
else if (msgType == MsgType.StartCountdown) |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.StartCountdown, null); |
|||
else if (msgType == MsgType.CancelCountdown) |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.CancelCountdown, null); |
|||
else if (msgType == MsgType.ConfirmInGame) |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ConfirmInGameState, null); |
|||
else if (msgType == MsgType.EndInGame) |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null); |
|||
|
|||
ProcessNetworkEventDataAdditional(conn, strm, msgType, id); |
|||
} |
|||
else if (cmd == NetworkEvent.Type.Disconnect) |
|||
ProcessDisconnectEvent(conn, strm); |
|||
} |
|||
|
|||
protected virtual void ProcessNetworkEventDataAdditional(NetworkConnection conn, DataStreamReader strm, MsgType msgType, string id) { } |
|||
protected virtual void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm) |
|||
{ |
|||
// The host disconnected, and Relay does not support host migration. So, all clients should disconnect.
|
|||
Debug.LogError("Host disconnected! Leaving the lobby."); |
|||
Leave(); |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeGameState, GameState.JoinMenu); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Relay uses raw pointers for efficiency. This converts them to byte arrays, assuming the stream contents are 1 byte for array length followed by contents.
|
|||
/// </summary>
|
|||
unsafe private string ReadLengthAndString(ref DataStreamReader strm) |
|||
{ |
|||
byte length = strm.ReadByte(); |
|||
byte[] bytes = new byte[length]; |
|||
fixed (byte* ptr = bytes) |
|||
{ |
|||
strm.ReadBytes(ptr, length); |
|||
} |
|||
return System.Text.Encoding.UTF8.GetString(bytes); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Once a client is connected, send a message out alerting the host.
|
|||
/// </summary>
|
|||
private void SendInitialMessage(NetworkDriver driver, NetworkConnection connection) |
|||
{ |
|||
WriteByte(driver, connection, m_localUser.ID, MsgType.NewPlayer, 0); |
|||
ForceFullUserUpdate(driver, connection, m_localUser); // Assuming this is only created after the Relay connection is successful.
|
|||
m_hasSentInitialMessage = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// When player data is updated, send out events for just the data that actually changed.
|
|||
/// </summary>
|
|||
private void DoUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user) |
|||
{ |
|||
// Only update with actual changes. (If multiple change at once, we send messages for each separately, but that shouldn't happen often.)
|
|||
if (0 < (user.LastChanged & LobbyUser.UserMembers.DisplayName)) |
|||
WriteString(driver, connection, user.ID, MsgType.PlayerName, user.DisplayName); |
|||
if (0 < (user.LastChanged & LobbyUser.UserMembers.Emote)) |
|||
WriteByte(driver, connection, user.ID, MsgType.Emote, (byte)user.Emote); |
|||
if (0 < (user.LastChanged & LobbyUser.UserMembers.UserStatus)) |
|||
WriteByte(driver, connection, user.ID, MsgType.ReadyState, (byte)user.UserStatus); |
|||
} |
|||
/// <summary>
|
|||
/// Sometimes (e.g. when a new player joins), we need to send out the full current state of this player.
|
|||
/// </summary>
|
|||
protected void ForceFullUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user) |
|||
{ |
|||
// Note that it would be better to send a single message with the full state, but for the sake of shorter code we'll leave that out here.
|
|||
WriteString(driver, connection, user.ID, MsgType.PlayerName, user.DisplayName); |
|||
WriteByte(driver, connection, user.ID, MsgType.Emote, (byte)user.Emote); |
|||
WriteByte(driver, connection, user.ID, MsgType.ReadyState, (byte)user.UserStatus); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write string data as: [1 byte: msgType] [1 byte: id length N] [N bytes: id] [1 byte: string length M] [M bytes: string]
|
|||
/// </summary>
|
|||
protected void WriteString(NetworkDriver driver, NetworkConnection connection, string id, MsgType msgType, string str) |
|||
{ |
|||
byte[] idBytes = System.Text.Encoding.UTF8.GetBytes(id); |
|||
byte[] strBytes = System.Text.Encoding.UTF8.GetBytes(str); |
|||
|
|||
List<byte> message = new List<byte>(idBytes.Length + strBytes.Length + 3); |
|||
message.Add((byte)msgType); |
|||
message.Add((byte)idBytes.Length); |
|||
message.AddRange(idBytes); |
|||
message.Add((byte)strBytes.Length); |
|||
message.AddRange(strBytes); |
|||
|
|||
if (driver.BeginSend(connection, out var dataStream) == 0) // Oh, should check this first?
|
|||
{ |
|||
byte[] bytes = message.ToArray(); |
|||
unsafe |
|||
{ |
|||
fixed (byte* bytesPtr = bytes) |
|||
{ |
|||
dataStream.WriteBytes(bytesPtr, message.Count); |
|||
driver.EndSend(dataStream); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write byte data as: [1 byte: msgType] [1 byte: id length N] [N bytes: id] [1 byte: data]
|
|||
/// </summary>
|
|||
protected void WriteByte(NetworkDriver driver, NetworkConnection connection, string id, MsgType msgType, byte value) |
|||
{ |
|||
byte[] idBytes = System.Text.Encoding.UTF8.GetBytes(id); |
|||
List<byte> message = new List<byte>(idBytes.Length + 3); |
|||
message.Add((byte)msgType); |
|||
message.Add((byte)idBytes.Length); |
|||
message.AddRange(idBytes); |
|||
message.Add(value); |
|||
|
|||
if (driver.BeginSend(connection, out var dataStream) == 0) // Oh, should check this first?
|
|||
{ |
|||
byte[] bytes = message.ToArray(); |
|||
unsafe |
|||
{ |
|||
fixed (byte* bytesPtr = bytes) |
|||
{ |
|||
dataStream.WriteBytes(bytesPtr, message.Count); |
|||
driver.EndSend(dataStream); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Leave() |
|||
{ |
|||
foreach (NetworkConnection connection in m_connections) |
|||
connection.Disconnect(m_networkDriver); |
|||
m_localLobby.RelayServer = null; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 57add7ba7b318f04a8781c247344cab8 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System.Collections.Generic; |
|||
using Unity.Networking.Transport; |
|||
using MsgType = LobbyRelaySample.relay.RelayUtpSetup.MsgType; |
|||
|
|||
namespace LobbyRelaySample.relay |
|||
{ |
|||
/// <summary>
|
|||
/// In addition to maintaining a heartbeat with the Relay server to keep it from timing out, the host player must pass network events
|
|||
/// from clients to all other clients, since they don't connect to each other.
|
|||
/// </summary>
|
|||
public class RelayUtpHost : RelayUtpClient, IReceiveMessages |
|||
{ |
|||
public override void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby) |
|||
{ |
|||
base.Initialize(networkDriver, connections, localUser, localLobby); |
|||
m_hasSentInitialMessage = true; // The host will be alone in the lobby at first, so they need not send any messages right away.
|
|||
Locator.Get.Messenger.Subscribe(this); |
|||
} |
|||
protected override void Uninitialize() |
|||
{ |
|||
base.Uninitialize(); |
|||
Locator.Get.Messenger.Unsubscribe(this); |
|||
} |
|||
|
|||
protected override void OnUpdate() |
|||
{ |
|||
base.OnUpdate(); |
|||
DoHeartbeat(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// When a new client connects, they need to be updated with the current state of everyone else.
|
|||
/// </summary>
|
|||
private void OnNewConnection(NetworkConnection conn) |
|||
{ |
|||
foreach (var user in m_localLobby.LobbyUsers) // The host includes itself here since we don't necessarily have an ID available, but it will ignore its own messages on arrival.
|
|||
ForceFullUserUpdate(m_networkDriver, conn, user.Value); |
|||
} |
|||
|
|||
protected override void ProcessNetworkEventDataAdditional(NetworkConnection conn, DataStreamReader strm, MsgType msgType, string id) |
|||
{ |
|||
// Note that the strm contents might have already been consumed, depending on the msgType.
|
|||
|
|||
// Forward messages from clients to other clients.
|
|||
if (msgType == MsgType.PlayerName) |
|||
{ |
|||
string name = m_localLobby.LobbyUsers[id].DisplayName; |
|||
foreach (NetworkConnection otherConn in m_connections) |
|||
{ |
|||
if (otherConn == conn) |
|||
continue; |
|||
WriteString(m_networkDriver, otherConn, id, msgType, name); |
|||
} |
|||
} |
|||
else if (msgType == MsgType.Emote || msgType == MsgType.ReadyState) |
|||
{ |
|||
byte value = msgType == MsgType.Emote ? (byte)m_localLobby.LobbyUsers[id].Emote : (byte)m_localLobby.LobbyUsers[id].UserStatus; |
|||
foreach (NetworkConnection otherConn in m_connections) |
|||
{ |
|||
if (otherConn == conn) |
|||
continue; |
|||
WriteByte(m_networkDriver, otherConn, id, msgType, value); |
|||
} |
|||
} |
|||
else if (msgType == MsgType.NewPlayer) // This ensures clients in builds are sent player state once they establish that they can send (and receive) events.
|
|||
OnNewConnection(conn); |
|||
|
|||
// If a client has changed state, check if this changes whether all players have readied.
|
|||
if (msgType == MsgType.ReadyState) |
|||
CheckIfAllUsersReady(); |
|||
} |
|||
|
|||
protected override void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm) |
|||
{ |
|||
// When a disconnect from the host occurs, no additional action is required. This override just prevents the base behavior from occurring.
|
|||
// TODO: If a client disconnects, see if remaining players are all already ready.
|
|||
UnityEngine.Debug.LogError("Client disconnected!"); |
|||
} |
|||
|
|||
public void OnReceiveMessage(MessageType type, object msg) |
|||
{ |
|||
if (type == MessageType.LobbyUserStatus) |
|||
CheckIfAllUsersReady(); |
|||
else if (type == MessageType.EndGame) // This assumes that only the host will have the End Game button available; otherwise, clients need to be able to send this message, too.
|
|||
{ |
|||
foreach (NetworkConnection connection in m_connections) |
|||
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.EndInGame, 0); |
|||
} |
|||
} |
|||
|
|||
private void CheckIfAllUsersReady() |
|||
{ |
|||
bool haveAllReadied = true; |
|||
foreach (var user in m_localLobby.LobbyUsers) |
|||
{ |
|||
if (user.Value.UserStatus != UserStatus.Ready) |
|||
{ haveAllReadied = false; |
|||
break; |
|||
} |
|||
} |
|||
if (haveAllReadied && m_localLobby.State == LobbyState.Lobby) // Need to notify both this client and all others that all players have readied.
|
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.StartCountdown, null); |
|||
foreach (NetworkConnection connection in m_connections) |
|||
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.StartCountdown, 0); |
|||
} |
|||
else if (!haveAllReadied && m_localLobby.State == LobbyState.CountDown) // Someone cancelled during the countdown, so abort the countdown.
|
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.CancelCountdown, null); |
|||
foreach (NetworkConnection connection in m_connections) |
|||
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.CancelCountdown, 0); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// In an actual game, after the countdown, there would be some step here where the host and all clients sync up on game state, load assets, etc.
|
|||
/// Here, we will instead just signal an "in-game" state that can be ended by the host.
|
|||
/// </summary>
|
|||
public void SendInGameState() |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ConfirmInGameState, null); |
|||
foreach (NetworkConnection connection in m_connections) |
|||
WriteByte(m_networkDriver, connection, m_localUser.ID, MsgType.ConfirmInGame, 0); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clean out destroyed connections, and accept all new ones.
|
|||
/// </summary>
|
|||
private void DoHeartbeat() |
|||
{ |
|||
for (int c = m_connections.Count - 1; c >= 0; c--) |
|||
{ |
|||
if (!m_connections[c].IsCreated) |
|||
m_connections.RemoveAt(c); |
|||
} |
|||
while (true) |
|||
{ |
|||
var conn = m_networkDriver.Accept(); // Note that since we pumped the event queue earlier in Update, m_networkDriver has been updated already this frame.
|
|||
if (!conn.IsCreated) // "Nothing more to accept" is signalled by returning an invalid connection from Accept.
|
|||
break; |
|||
m_connections.Add(conn); |
|||
OnNewConnection(conn); // This ensures that clients in editors are sent player state once they establish a connection. The timing differs slightly from builds.
|
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 567f8fffb5a28a446b1e98cbd2510b0f |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Unity.Networking.Transport; |
|||
using Unity.Networking.Transport.Relay; |
|||
using Unity.Services.Relay.Models; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.relay |
|||
{ |
|||
/// <summary>
|
|||
/// Responsible for setting up a connection with Relay using UTP, for the lobby host.
|
|||
/// Must be a MonoBehaviour since the binding process doesn't have asynchronous callback options.
|
|||
/// </summary>
|
|||
public abstract class RelayUtpSetup : MonoBehaviour |
|||
{ |
|||
protected bool m_isRelayConnected = false; |
|||
protected NetworkDriver m_networkDriver; |
|||
protected List<NetworkConnection> m_connections; |
|||
protected NetworkEndPoint m_endpointForServer; |
|||
protected LocalLobby m_localLobby; |
|||
protected LobbyUser m_localUser; |
|||
protected Action<bool, RelayUtpClient> m_onJoinComplete; |
|||
|
|||
public enum MsgType { Ping = 0, NewPlayer, ReadyState, PlayerName, Emote, StartCountdown, CancelCountdown, ConfirmInGame, EndInGame } |
|||
|
|||
public void BeginRelayJoin(LocalLobby localLobby, LobbyUser localUser, Action<bool, RelayUtpClient> onJoinComplete) |
|||
{ |
|||
m_localLobby = localLobby; |
|||
m_localUser = localUser; |
|||
m_onJoinComplete = onJoinComplete; |
|||
JoinRelay(); |
|||
} |
|||
protected abstract void JoinRelay(); |
|||
|
|||
/// <summary>
|
|||
/// Shared behavior for binding to the Relay allocation, which is required for use.
|
|||
/// Note that a host will send bytes from the Allocation it creates, whereas a client will send bytes from the JoinAllocation it receives using a relay code.
|
|||
/// </summary>
|
|||
protected void BindToAllocation(string ip, int port, byte[] allocationIdBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes, byte[] hmacKeyBytes, int connectionCapacity) |
|||
{ |
|||
NetworkEndPoint serverEndpoint = NetworkEndPoint.Parse(ip, (ushort)port); |
|||
RelayAllocationId allocationId = ConvertAllocationIdBytes(allocationIdBytes); |
|||
RelayConnectionData connectionData = ConvertConnectionDataBytes(connectionDataBytes); |
|||
RelayConnectionData hostConnectionData = ConvertConnectionDataBytes(hostConnectionDataBytes); |
|||
RelayHMACKey key = ConvertHMACKeyBytes(hmacKeyBytes); |
|||
|
|||
m_endpointForServer = serverEndpoint; |
|||
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref hostConnectionData, ref key); |
|||
relayServerData.ComputeNewNonce(); |
|||
var relayNetworkParameter = new RelayNetworkParameter { ServerData = relayServerData }; |
|||
|
|||
m_networkDriver = NetworkDriver.Create(new INetworkParameter[] { relayNetworkParameter }); |
|||
m_connections = new List<NetworkConnection>(connectionCapacity); |
|||
|
|||
if (m_networkDriver.Bind(NetworkEndPoint.AnyIpv4) != 0) |
|||
Debug.LogError("Failed to bind to Relay allocation."); |
|||
else |
|||
StartCoroutine(WaitForBindComplete()); |
|||
} |
|||
|
|||
private IEnumerator WaitForBindComplete() |
|||
{ |
|||
while (!m_networkDriver.Bound) |
|||
{ |
|||
m_networkDriver.ScheduleUpdate().Complete(); |
|||
yield return null; |
|||
} |
|||
OnBindingComplete(); |
|||
} |
|||
|
|||
protected abstract void OnBindingComplete(); |
|||
|
|||
#region UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them.
|
|||
unsafe private static RelayAllocationId ConvertAllocationIdBytes(byte[] allocationIdBytes) |
|||
{ |
|||
fixed (byte* ptr = allocationIdBytes) |
|||
{ |
|||
return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length); |
|||
} |
|||
} |
|||
|
|||
unsafe private static RelayConnectionData ConvertConnectionDataBytes(byte[] connectionData) |
|||
{ |
|||
fixed (byte* ptr = connectionData) |
|||
{ |
|||
return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length); |
|||
} |
|||
} |
|||
|
|||
unsafe private static RelayHMACKey ConvertHMACKeyBytes(byte[] hmac) |
|||
{ |
|||
fixed (byte* ptr = hmac) |
|||
{ |
|||
return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length); |
|||
} |
|||
} |
|||
#endregion
|
|||
|
|||
private void OnDestroy() |
|||
{ |
|||
if (!m_isRelayConnected && m_networkDriver.IsCreated) |
|||
m_networkDriver.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Host logic: Request a new Allocation, and then both bind to it and request a join code. Once those are both complete, supply data back to the lobby.
|
|||
/// </summary>
|
|||
public class RelayUtpSetupHost : RelayUtpSetup |
|||
{ |
|||
[Flags] |
|||
private enum JoinState { None = 0, Bound = 1, Joined = 2 } |
|||
private JoinState m_joinState = JoinState.None; |
|||
private Allocation m_allocation; |
|||
|
|||
protected override void JoinRelay() |
|||
{ |
|||
RelayAPIInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnAllocation); |
|||
} |
|||
|
|||
private void OnAllocation(Allocation allocation) |
|||
{ |
|||
m_allocation = allocation; |
|||
RelayAPIInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode); |
|||
BindToAllocation(allocation.RelayServer.IpV4, allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.ConnectionData, allocation.ConnectionData, allocation.Key, 16); |
|||
} |
|||
|
|||
private void OnRelayCode(string relayCode) |
|||
{ |
|||
m_localLobby.RelayCode = relayCode; |
|||
m_localLobby.RelayServer = new ServerAddress(m_allocation.RelayServer.IpV4, m_allocation.RelayServer.Port); |
|||
m_joinState |= JoinState.Joined; |
|||
CheckForComplete(); |
|||
} |
|||
|
|||
protected override void OnBindingComplete() |
|||
{ |
|||
if (m_networkDriver.Listen() != 0) |
|||
{ |
|||
Debug.LogError("Server failed to listen"); |
|||
m_onJoinComplete(false, null); |
|||
} |
|||
else |
|||
{ |
|||
Debug.LogWarning("Server is now listening!"); |
|||
m_joinState |= JoinState.Bound; |
|||
CheckForComplete(); |
|||
} |
|||
} |
|||
|
|||
private void CheckForComplete() |
|||
{ |
|||
if (m_joinState == (JoinState.Joined | JoinState.Bound)) |
|||
{ |
|||
m_isRelayConnected = true; |
|||
RelayUtpHost host = gameObject.AddComponent<RelayUtpHost>(); |
|||
host.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby); |
|||
m_onJoinComplete(true, host); |
|||
LobbyAsyncRequests.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode, null); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Client logic: Wait until the Relay join code is retrieved from the lobby's shared data. Then, use that code to get the Allocation to bind to, and
|
|||
/// then create a connection to the host.
|
|||
/// </summary>
|
|||
public class RelayUtpSetupClient : RelayUtpSetup |
|||
{ |
|||
private JoinAllocation m_allocation; |
|||
|
|||
protected override void JoinRelay() |
|||
{ |
|||
m_localLobby.onChanged += OnLobbyChange; |
|||
} |
|||
|
|||
private void OnLobbyChange(LocalLobby lobby) |
|||
{ |
|||
if (m_localLobby.RelayCode != null) |
|||
{ |
|||
RelayAPIInterface.JoinAsync(m_localLobby.RelayCode, OnJoin); |
|||
m_localLobby.onChanged -= OnLobbyChange; |
|||
} |
|||
} |
|||
|
|||
private void OnJoin(JoinAllocation joinAllocation) |
|||
{ |
|||
if (joinAllocation == null) |
|||
return; |
|||
m_allocation = joinAllocation; |
|||
BindToAllocation(joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, joinAllocation.AllocationIdBytes, joinAllocation.ConnectionData, joinAllocation.HostConnectionData, joinAllocation.Key, 1); |
|||
m_localLobby.RelayServer = new ServerAddress(joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port); |
|||
} |
|||
|
|||
protected override void OnBindingComplete() |
|||
{ |
|||
StartCoroutine(ConnectToServer()); |
|||
} |
|||
|
|||
private IEnumerator ConnectToServer() |
|||
{ |
|||
// Once the client is bound to the Relay server, send a connection request.
|
|||
m_connections.Add(m_networkDriver.Connect(m_endpointForServer)); |
|||
while (m_networkDriver.GetConnectionState(m_connections[0]) == NetworkConnection.State.Connecting) |
|||
{ |
|||
m_networkDriver.ScheduleUpdate().Complete(); |
|||
yield return null; |
|||
} |
|||
if (m_networkDriver.GetConnectionState(m_connections[0]) != NetworkConnection.State.Connected) |
|||
{ |
|||
Debug.LogError("Client failed to connect to server"); |
|||
m_onJoinComplete(false, null); |
|||
} |
|||
else |
|||
{ |
|||
m_isRelayConnected = true; |
|||
RelayUtpClient client = gameObject.AddComponent<RelayUtpClient>(); |
|||
client.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby); |
|||
m_onJoinComplete(true, client); |
|||
LobbyAsyncRequests.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode, null); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 5857e5666b1ecf844b8280729adb6e6e |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using LobbyRelaySample.UI; |
|||
using TMPro; |
|||
using UnityEngine; |
|||
using UnityEngine.Serialization; |
|||
using UnityEngine.UI; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public class PopUpUI : MonoBehaviour |
|||
{ |
|||
[SerializeField] |
|||
TMP_InputField m_popupText; |
|||
|
|||
public void ShowPopup(string newText, Color textColor = default) |
|||
{ |
|||
m_popupText.SetTextWithoutNotify(newText); |
|||
m_popupText.textComponent.color = textColor; |
|||
} |
|||
|
|||
public void Delete() |
|||
{ |
|||
Destroy(gameObject); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: e5f1bc94a7a7b5249a02001abc3096ef |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Observes the Lobby request rate limits and changes the visibility of a UIPanelBase to suit.
|
|||
/// E.g. the refresh button on the Join menu should be inactive after a refresh for long enough to avoid the lobby query rate limit.
|
|||
/// </summary>
|
|||
public class RateLimitVisibility : MonoBehaviour |
|||
{ |
|||
[SerializeField] |
|||
UIPanelBase m_target; |
|||
[SerializeField] |
|||
float m_alphaWhenHidden = 0.5f; |
|||
[SerializeField] |
|||
LobbyAsyncRequests.RequestType m_requestType; |
|||
|
|||
private void Start() |
|||
{ |
|||
LobbyAsyncRequests.Instance.GetRateLimit(m_requestType).onChanged += UpdateVisibility; |
|||
} |
|||
private void OnDestroy() |
|||
{ |
|||
LobbyAsyncRequests.Instance.GetRateLimit(m_requestType).onChanged -= UpdateVisibility; |
|||
} |
|||
|
|||
private void UpdateVisibility(LobbyAsyncRequests.RateLimitCooldown rateLimit) |
|||
{ |
|||
if (rateLimit.IsInCooldown) |
|||
m_target.Hide(m_alphaWhenHidden); |
|||
else |
|||
m_target.Show(); |
|||
} |
|||
} |
|||
} |
|
|||
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; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 4079cd003fcd20c40a3bac78acf44b55 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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" : |
|||
""; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7aacd044390df3c4ca5cd037fec35483 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using LobbyRelaySample.relay; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Sets up and runs the entire sample.
|
|||
/// </summary>
|
|||
public class GameManager : MonoBehaviour, IReceiveMessages |
|||
{ |
|||
/// <summary>
|
|||
/// All these should be assigned the observers in the scene at the start.
|
|||
/// </summary>
|
|||
|
|||
#region UI elements that observe the local state. These are
|
|||
|
|||
[SerializeField] |
|||
private List<LocalGameStateObserver> m_GameStateObservers = new List<LocalGameStateObserver>(); |
|||
[SerializeField] |
|||
private List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>(); |
|||
[SerializeField] |
|||
private List<LobbyUserObserver> m_LocalUserObservers = new List<LobbyUserObserver>(); |
|||
[SerializeField] |
|||
private List<LobbyServiceDataObserver> m_LobbyServiceObservers = new List<LobbyServiceDataObserver>(); |
|||
|
|||
#endregion
|
|||
|
|||
private LocalGameState m_localGameState = new LocalGameState(); |
|||
private LobbyUser m_localUser; |
|||
private LocalLobby m_localLobby; |
|||
private LobbyServiceData m_lobbyServiceData = new LobbyServiceData(); |
|||
private LobbyContentHeartbeat m_lobbyContentHeartbeat = new LobbyContentHeartbeat(); |
|||
private RelayUtpSetup m_relaySetup; |
|||
private RelayUtpClient m_relayClient; |
|||
|
|||
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
|
|||
public void SetLobbyColorFilter(int color) |
|||
{ |
|||
m_lobbyColorFilter = (LobbyColor)color; |
|||
} |
|||
|
|||
private LobbyColor m_lobbyColorFilter; |
|||
|
|||
#region Setup
|
|||
|
|||
private void Awake() |
|||
{ |
|||
// Do some arbitrary operations to instantiate singletons.
|
|||
#pragma warning disable IDE0059 // Unnecessary assignment of a value
|
|||
var unused = Locator.Get; |
|||
#pragma warning restore IDE0059
|
|||
|
|||
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn)); |
|||
Application.wantsToQuit += OnWantToQuit; |
|||
} |
|||
|
|||
private void Start() |
|||
{ |
|||
m_localLobby = new LocalLobby { State = LobbyState.Lobby }; |
|||
m_localUser = new LobbyUser(); |
|||
m_localUser.DisplayName = "New Player"; |
|||
Locator.Get.Messenger.Subscribe(this); |
|||
BeginObservers(); |
|||
} |
|||
|
|||
private void OnAuthSignIn() |
|||
{ |
|||
Debug.Log("Signed in."); |
|||
m_localUser.ID = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id"); |
|||
m_localUser.DisplayName = NameGenerator.GetName(m_localUser.ID); |
|||
m_localLobby.AddPlayer(m_localUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.
|
|||
} |
|||
|
|||
private void BeginObservers() |
|||
{ |
|||
foreach (var gameStateObs in m_GameStateObservers) |
|||
gameStateObs.BeginObserving(m_localGameState); |
|||
foreach (var serviceObs in m_LobbyServiceObservers) |
|||
serviceObs.BeginObserving(m_lobbyServiceData); |
|||
foreach (var lobbyObs in m_LocalLobbyObservers) |
|||
lobbyObs.BeginObserving(m_localLobby); |
|||
foreach (var userObs in m_LocalUserObservers) |
|||
userObs.BeginObserving(m_localUser); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
/// <summary>
|
|||
/// Primarily used for UI elements to communicate state changes, this will receive messages from arbitrary providers for user interactions.
|
|||
/// </summary>
|
|||
public void OnReceiveMessage(MessageType type, object msg) |
|||
{ |
|||
if (type == MessageType.RenameRequest) |
|||
{ m_localUser.DisplayName = (string)msg; |
|||
} |
|||
else if (type == MessageType.CreateLobbyRequest) |
|||
{ |
|||
var createLobbyData = (LocalLobby)msg; |
|||
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) => |
|||
{ lobby.ToLocalLobby.Convert(r, m_localLobby); |
|||
OnCreatedLobby(); |
|||
}, |
|||
OnFailedJoin); |
|||
} |
|||
else if (type == MessageType.JoinLobbyRequest) |
|||
{ |
|||
LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg; |
|||
LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, m_localUser, (r) => |
|||
{ lobby.ToLocalLobby.Convert(r, m_localLobby); |
|||
OnJoinedLobby(); |
|||
}, |
|||
OnFailedJoin); |
|||
} |
|||
else if (type == MessageType.QueryLobbies) |
|||
{ |
|||
m_lobbyServiceData.State = LobbyQueryState.Fetching; |
|||
LobbyAsyncRequests.Instance.RetrieveLobbyListAsync( |
|||
qr => { |
|||
if (qr != null) |
|||
OnLobbiesQueried(lobby.ToLocalLobby.Convert(qr)); |
|||
}, |
|||
er => { |
|||
OnLobbyQueryFailed(); |
|||
}, |
|||
m_lobbyColorFilter); |
|||
} |
|||
else if (type == MessageType.ChangeGameState) |
|||
{ SetGameState((GameState)msg); |
|||
} |
|||
else if (type == MessageType.UserSetEmote) |
|||
{ EmoteType emote = (EmoteType)msg; |
|||
m_localUser.Emote = emote; |
|||
} |
|||
else if (type == MessageType.LobbyUserStatus) |
|||
{ m_localUser.UserStatus = (UserStatus)msg; |
|||
} |
|||
else if (type == MessageType.StartCountdown) |
|||
{ BeginCountDown(); |
|||
} |
|||
else if (type == MessageType.CancelCountdown) |
|||
{ m_localLobby.State = LobbyState.Lobby; |
|||
m_localLobby.CountDownTime = 0; |
|||
} |
|||
else if (type == MessageType.ConfirmInGameState) |
|||
{ m_localUser.UserStatus = UserStatus.InGame; |
|||
m_localLobby.State = LobbyState.InGame; |
|||
} |
|||
else if (type == MessageType.EndGame) |
|||
{ m_localLobby.State = LobbyState.Lobby; |
|||
m_localLobby.CountDownTime = 0; |
|||
SetUserLobbyState(); |
|||
} |
|||
} |
|||
|
|||
private void SetGameState(GameState state) |
|||
{ |
|||
bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) && m_localGameState.State == GameState.Lobby; |
|||
m_localGameState.State = state; |
|||
if (isLeavingLobby) |
|||
OnLeftLobby(); |
|||
} |
|||
|
|||
private void OnLobbiesQueried(IEnumerable<LocalLobby> lobbies) |
|||
{ |
|||
var newLobbyDict = new Dictionary<string, LocalLobby>(); |
|||
foreach (var lobby in lobbies) |
|||
newLobbyDict.Add(lobby.LobbyID, lobby); |
|||
|
|||
m_lobbyServiceData.State = LobbyQueryState.Fetched; |
|||
m_lobbyServiceData.CurrentLobbies = newLobbyDict; |
|||
} |
|||
|
|||
private void OnLobbyQueryFailed() |
|||
{ |
|||
m_lobbyServiceData.State = LobbyQueryState.Error; |
|||
} |
|||
|
|||
private void OnCreatedLobby() |
|||
{ |
|||
m_localUser.IsHost = true; |
|||
OnJoinedLobby(); |
|||
} |
|||
|
|||
private void OnJoinedLobby() |
|||
{ |
|||
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID); |
|||
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser); |
|||
SetUserLobbyState(); |
|||
StartRelayConnection(); |
|||
} |
|||
|
|||
private void OnLeftLobby() |
|||
{ |
|||
m_localUser.ResetState(); |
|||
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID, ResetLocalLobby); |
|||
m_lobbyContentHeartbeat.EndTracking(); |
|||
LobbyAsyncRequests.Instance.EndTracking(); |
|||
|
|||
if (m_relaySetup != null) |
|||
{ |
|||
Component.Destroy(m_relaySetup); |
|||
m_relaySetup = null; |
|||
} |
|||
|
|||
if (m_relayClient != null) |
|||
{ |
|||
Component.Destroy(m_relayClient); |
|||
m_relayClient = null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Back to Join menu if we fail to join for whatever reason.
|
|||
/// </summary>
|
|||
private void OnFailedJoin() |
|||
{ |
|||
SetGameState(GameState.JoinMenu); |
|||
} |
|||
|
|||
private void StartRelayConnection() |
|||
{ |
|||
if (m_localUser.IsHost) |
|||
m_relaySetup = gameObject.AddComponent<RelayUtpSetupHost>(); |
|||
else |
|||
m_relaySetup = gameObject.AddComponent<RelayUtpSetupClient>(); |
|||
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting); |
|||
m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected); |
|||
} |
|||
|
|||
private void OnRelayConnected(bool didSucceed, RelayUtpClient client) |
|||
{ |
|||
Component.Destroy(m_relaySetup); |
|||
m_relaySetup = null; |
|||
|
|||
if (!didSucceed) |
|||
{ |
|||
Debug.LogError("Relay connection failed! Retrying in 5s..."); |
|||
StartCoroutine(RetryRelayConnection()); |
|||
return; |
|||
} |
|||
|
|||
m_relayClient = client; |
|||
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby); |
|||
} |
|||
|
|||
private IEnumerator RetryRelayConnection() |
|||
{ |
|||
yield return new WaitForSeconds(5); |
|||
StartRelayConnection(); |
|||
} |
|||
|
|||
private void BeginCountDown() |
|||
{ |
|||
if (m_localLobby.State == LobbyState.CountDown) |
|||
return; |
|||
m_localLobby.CountDownTime = 4; |
|||
m_localLobby.State = LobbyState.CountDown; |
|||
StartCoroutine(CountDown()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The CountdownUI will pick up on changes to the lobby's countdown timer. This can be interrupted if the lobby leaves the countdown state (via a CancelCountdown message).
|
|||
/// </summary>
|
|||
private IEnumerator CountDown() |
|||
{ |
|||
while (m_localLobby.CountDownTime > 0) |
|||
{ |
|||
yield return null; |
|||
if (m_localLobby.State != LobbyState.CountDown) |
|||
yield break; |
|||
m_localLobby.CountDownTime -= Time.deltaTime; |
|||
} |
|||
|
|||
if (m_relayClient is RelayUtpHost) |
|||
(m_relayClient as RelayUtpHost).SendInGameState(); |
|||
} |
|||
|
|||
private void SetUserLobbyState() |
|||
{ |
|||
SetGameState(GameState.Lobby); |
|||
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby); |
|||
} |
|||
|
|||
private void ResetLocalLobby() |
|||
{ |
|||
m_localLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary<string, LobbyUser>()); |
|||
m_localLobby.AddPlayer(m_localUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
|
|||
m_localLobby.CountDownTime = 0; |
|||
m_localLobby.RelayServer = null; |
|||
} |
|||
|
|||
#region Teardown
|
|||
|
|||
/// <summary>
|
|||
/// In builds, if we are in a lobby and try to send a Leave request on application quit, it won't go through if we're quitting on the same frame.
|
|||
/// So, we need to delay just briefly to let the request happen (though we don't need to wait for the result).
|
|||
/// </summary>
|
|||
private IEnumerator LeaveBeforeQuit() |
|||
{ |
|||
ForceLeaveAttempt(); |
|||
yield return null; |
|||
Application.Quit(); |
|||
} |
|||
|
|||
private bool OnWantToQuit() |
|||
{ |
|||
bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID); |
|||
StartCoroutine(LeaveBeforeQuit()); |
|||
return canQuit; |
|||
} |
|||
|
|||
private void OnDestroy() |
|||
{ |
|||
ForceLeaveAttempt(); |
|||
} |
|||
|
|||
private void ForceLeaveAttempt() |
|||
{ |
|||
Locator.Get.Messenger.Unsubscribe(this); |
|||
if (!string.IsNullOrEmpty(m_localLobby?.LobbyID)) |
|||
{ |
|||
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID, null); |
|||
m_localLobby = null; |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
} |
|||
} |
|
|||
{ |
|||
"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 |
|||
} |
|||
} |
|
|||
{ |
|||
"MonoBehaviour": { |
|||
"Version": 3, |
|||
"DisabledWarnings": "" |
|||
} |
|||
} |
1001
README.pdf
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
1001
~Documentation/Images/1_lobby_list.PNG
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
1001
~Documentation/Images/2_lobby.PNG
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
|
|||
fileFormatVersion: 2 |
|||
guid: 57c53a150c76b1f4c9fe27ee2b9a1d17 |
|||
TextureImporter: |
|||
internalIDToNameTable: [] |
|||
externalObjects: {} |
|||
serializedVersion: 11 |
|||
mipmaps: |
|||
mipMapMode: 0 |
|||
enableMipMap: 0 |
|||
sRGBTexture: 1 |
|||
linearTexture: 0 |
|||
fadeOut: 0 |
|||
borderMipMap: 0 |
|||
mipMapsPreserveCoverage: 0 |
|||
alphaTestReferenceValue: 0.5 |
|||
mipMapFadeDistanceStart: 1 |
|||
mipMapFadeDistanceEnd: 3 |
|||
bumpmap: |
|||
convertToNormalMap: 0 |
|||
externalNormalMap: 0 |
|||
heightScale: 0.25 |
|||
normalMapFilter: 0 |
|||
isReadable: 0 |
|||
streamingMipmaps: 0 |
|||
streamingMipmapsPriority: 0 |
|||
vTOnly: 0 |
|||
grayScaleToAlpha: 0 |
|||
generateCubemap: 6 |
|||
cubemapConvolution: 0 |
|||
seamlessCubemap: 0 |
|||
textureFormat: 1 |
|||
maxTextureSize: 2048 |
|||
textureSettings: |
|||
serializedVersion: 2 |
|||
filterMode: 1 |
|||
aniso: 1 |
|||
mipBias: 0 |
|||
wrapU: 1 |
|||
wrapV: 1 |
|||
wrapW: 1 |
|||
nPOTScale: 0 |
|||
lightmap: 0 |
|||
compressionQuality: 50 |
|||
spriteMode: 1 |
|||
spriteExtrude: 1 |
|||
spriteMeshType: 1 |
|||
alignment: 0 |
|||
spritePivot: {x: 0.5, y: 0.5} |
|||
spritePixelsToUnits: 100 |
|||
spriteBorder: {x: 0, y: 0, z: 0, w: 0} |
|||
spriteGenerateFallbackPhysicsShape: 1 |
|||
alphaUsage: 1 |
|||
alphaIsTransparency: 1 |
|||
spriteTessellationDetail: -1 |
|||
textureType: 8 |
|||
textureShape: 1 |
|||
singleChannelComponent: 0 |
|||
flipbookRows: 1 |
|||
flipbookColumns: 1 |
|||
maxTextureSizeSet: 0 |
|||
compressionQualitySet: 0 |
|||
textureFormatSet: 0 |
|||
ignorePngGamma: 0 |
|||
applyGammaDecoding: 0 |
|||
platformSettings: |
|||
- serializedVersion: 3 |
|||
buildTarget: DefaultTexturePlatform |
|||
maxTextureSize: 2048 |
|||
resizeAlgorithm: 0 |
|||
textureFormat: -1 |
|||
textureCompression: 1 |
|||
compressionQuality: 50 |
|||
crunchedCompression: 0 |
|||
allowsAlphaSplitting: 0 |
|||
overridden: 0 |
|||
androidETC2FallbackOverride: 0 |
|||
forceMaximumCompressionQuality_BC6H_BC7: 0 |
|||
spriteSheet: |
|||
serializedVersion: 2 |
|||
sprites: [] |
|||
outline: [] |
|||
physicsShape: [] |
|||
bones: [] |
|||
spriteID: 5e97eb03825dee720800000000000000 |
|||
internalID: 0 |
|||
vertices: [] |
|||
indices: |
|||
edges: [] |
|||
weights: [] |
|||
secondaryTextures: [] |
|||
spritePackingTag: |
|||
pSDRemoveMatte: 0 |
|||
pSDShowRemoveMatteOption: 0 |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
* text=auto eol=lf |
|||
**/HavokNative.framework/HavokNative filter=lfs diff=lfs merge=lfs -text |
|||
**/cwebp filter=lfs diff=lfs merge=lfs -text |
|||
**/moz-cjpeg filter=lfs diff=lfs merge=lfs -text |
|||
**/pngcrush filter=lfs diff=lfs merge=lfs -text |
|||
*.3[dS][sS] filter=lfs diff=lfs merge=lfs -text |
|||
*.DOC diff=astextplain |
|||
*.DOCX diff=astextplain |
|||
*.DOT diff=astextplain |
|||
*.RTF diff=astextplain |
|||
*.[aA] filter=lfs diff=lfs merge=lfs -text |
|||
*.[aA][iI][fF][cC] filter=lfs diff=lfs merge=lfs -text |
|||
*.[aA][iI][fF][fF]? filter=lfs diff=lfs merge=lfs -text |
|||
*.[aA][pP][kK] filter=lfs diff=lfs merge=lfs -text |
|||
*.[aA][vV][iI] filter=lfs diff=lfs merge=lfs -text |
|||
*.[bB][lL][eE][nN][dD] filter=lfs diff=lfs merge=lfs -text |
|||
*.[cC][oO][lL][lL][aA][dD][dD][aA] filter=lfs diff=lfs merge=lfs -text |
|||
*.[dD][lL][lL] filter=lfs diff=lfs merge=lfs -text |
|||
*.[dD][yY][lL][iI][bB] filter=lfs diff=lfs merge=lfs -text |
|||
*.[eE][xX][eE] filter=lfs diff=lfs merge=lfs -text |
|||
*.[fF][bB][xX] filter=lfs diff=lfs merge=lfs -text |
|||
*.[fF][lL][aA] filter=lfs diff=lfs merge=lfs -text |
|||
*.[fF][lL][aA][cC] filter=lfs diff=lfs merge=lfs -text |
|||
*.[fF][lL][vV] filter=lfs diff=lfs merge=lfs -text |
|||
*.[gG][iI][fF] filter=lfs diff=lfs merge=lfs -text |
|||
*.[gG][zZ][iI][pP] filter=lfs diff=lfs merge=lfs -text |
|||
*.[iI][pP][aA] filter=lfs diff=lfs merge=lfs -text |
|||
*.[jJ][aA][rR] filter=lfs diff=lfs merge=lfs -text |
|||
*.[jJ][pP][gG] filter=lfs diff=lfs merge=lfs -text |
|||
*.[mM]4[vV] filter=lfs diff=lfs merge=lfs -text |
|||
*.[mM][kK][vV] filter=lfs diff=lfs merge=lfs -text |
|||
*.[mM][oO][vV] filter=lfs diff=lfs merge=lfs -text |
|||
*.[mM][pP]3 filter=lfs diff=lfs merge=lfs -text |
|||
*.[mM][pP][2-4]? filter=lfs diff=lfs merge=lfs -text |
|||
*.[mM][pP][eE]?[gG] filter=lfs diff=lfs merge=lfs -text |
|||
*.[oO][bB][jJ] filter=lfs diff=lfs merge=lfs -text |
|||
*.[oO][gG][gG] filter=lfs diff=lfs merge=lfs -text |
|||
*.[oO][gG][vV] filter=lfs diff=lfs merge=lfs -text |
|||
*.[oO][tT][fF] filter=lfs diff=lfs merge=lfs -text |
|||
*.[pP][dD][fF] filter=lfs diff=lfs merge=lfs -text |
|||
*.[pP][nN][gG] filter=lfs diff=lfs merge=lfs -text |
|||
*.[pP][sS][dD] filter=lfs diff=lfs merge=lfs -text |
|||
*.[rR][aA][rR] filter=lfs diff=lfs merge=lfs -text |
|||
*.[sS][oO] filter=lfs diff=lfs merge=lfs -text |
|||
*.[sS][tT][lL] filter=lfs diff=lfs merge=lfs -text |
|||
*.[sS][wW][fF] filter=lfs diff=lfs merge=lfs -text |
|||
*.[tT][aA][rR] filter=lfs diff=lfs merge=lfs -text |
|||
*.[tT][gG][zZ] filter=lfs diff=lfs merge=lfs -text |
|||
*.[tT][tT][fF] filter=lfs diff=lfs merge=lfs -text |
|||
*.[wW][aA][vV] filter=lfs diff=lfs merge=lfs -text |
|||
*.[wW][eE][bB][mM] filter=lfs diff=lfs merge=lfs -text |
|||
*.[zZ][iI][pP] filter=lfs diff=lfs merge=lfs -text |
|||
*.aif filter=lfs diff=lfs merge=lfs -text |
|||
*.anim filter=lfs diff=lfs merge=lfs -text |
|||
*.api eol=lf text |
|||
*.asset text |
|||
*.bundle filter=lfs diff=lfs merge=lfs -text |
|||
*.cginc text |
|||
*.compute text |
|||
*.compute text |
|||
*.cs diff=csharp text |
|||
*.dat filter=lfs diff=lfs merge=lfs -text |
|||
*.doc diff=astextplain |
|||
*.docx diff=astextplain |
|||
*.dot diff=astextplain |
|||
*.entities filter=lfs diff=lfs merge=lfs -text |
|||
*.entityheader filter=lfs diff=lfs merge=lfs -text |
|||
*.exr filter=lfs diff=lfs merge=lfs -text |
|||
*.gradle text eol=lf |
|||
*.mat text |
|||
*.md text |
|||
*.md5 text |
|||
*.meta text |
|||
*.pdb filter=lfs diff=lfs merge=lfs -text |
|||
*.prefab text |
|||
*.psb filter=lfs diff=lfs merge=lfs -text |
|||
*.raw filter=lfs diff=lfs merge=lfs -text |
|||
*.rtf diff=astextplain |
|||
*.shader text |
|||
*.tga filter=lfs diff=lfs merge=lfs -text |
|||
*.tif filter=lfs diff=lfs merge=lfs -text |
|||
*.tt eol=crlf text |
|||
*.txt text |
|||
*.unitypackage filter=lfs diff=lfs merge=lfs -text |
|||
*.yaml eol=lf |
|||
*.yml eol=lf |
|||
*/ProjectSettings/*.asset text |
|||
Havok.Vdb.dll filter=lfs diff=lfs merge=lfs -text |
|||
HavokNative.dll filter=lfs diff=lfs merge=lfs -text |
|||
HavokVisualDebugger.exe filter=lfs diff=lfs merge=lfs -text |
|
|||
[submodule "Tools/recipe-engine"] |
|||
path = Tools~/recipe-engine |
|||
url = ../recipe-engine.git |
|
|||
# Change log |
|||
|
|||
## [0.9.0] - 2021-05-10 |
|||
### New features |
|||
* Added support for long serialization and delta compression. |
|||
* Upgraded collections to 1.0.0 |
|||
* Added a new network interface for WebSockets, can be used in both native and web builds. |
|||
|
|||
### Changes |
|||
* Minimum required Unity version has changed to 2020.3.0f1. |
|||
* The transport package can be compiled with the tiny c# profile and for WebGL, but WebGL builds only support IPC - not sockets. |
|||
|
|||
### Fixes |
|||
### Upgrade guide |
|||
|
|||
## [0.8.0] - 2021-03-23 |
|||
### New features |
|||
* Added overloads of `PopEvent` and `PopEventForConnection` which return the pipeline used as an out parameter. |
|||
|
|||
### Changes |
|||
|
|||
### Fixes |
|||
* Fixed some compatility issues with tiny. |
|||
* Fixed a crash when sending messages slightly less than one MTU using the fragmentation pipeline. |
|||
* Fixed a bug causing `NetworkDriver.RemoteEndPoint` to return an invalid value when using the default network interface. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.7.0] - 2021-02-05 |
|||
### New features |
|||
* Added `DataStreamWriter.WriteRawbits` and `DataStreamWriter.ReadRawBits` for reading and writing raw bits from a data stream. |
|||
|
|||
### Changes |
|||
* Optimized the `NetworkCompressionModel` to find buckets in constant time. |
|||
* Changed the error behavior of `DataStreamReader` to be consistent between the editor and players. |
|||
|
|||
### Fixes |
|||
* Fixed a crash when receiving a packet with an invalid pipeline identifier. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.6.0] - 2020-11-26 |
|||
### New features |
|||
* An error handling pass has been made and `Error.StatusCode` have been added to indicate more specific errors. |
|||
* `Error.DisconnectReason` has been added, so when NetworkDriver.PopEvent returns a `NetworkEvent.Type.Disconnect` the reader returned contains 1 byte of data indicating the reason. |
|||
|
|||
### Changes |
|||
* The function signature for NetworkDriver.BeginSend has changed. It now returns an `int` value indicating if the function succeeded or not and the DataStreamWriter now instead is returned as a `out` parameter. |
|||
* The function signature for INetworkInterface.Initialize has changed. It now requires you to return an `int` value indicating if the function succeeded or not. |
|||
* The function signature for INetworkInterface.CreateInterfaceEndPoint has changed. It now requires you to return an `int` value indicating if the function succeeded or not, and NetworkInterfaceEndPoint is now returned as a `out` parameter. |
|||
|
|||
### Fixes |
|||
* Fixed a potential crash when receiving a malformated packet. |
|||
* Fixed an issue where the DataStream could sometimes fail writing packet uints before the buffer was full. |
|||
|
|||
### Upgrade guide |
|||
* `NetworkDriver.BeginSend` now returns an `int` indicating a `Error.StatusCode`, and the `DataStreamWriter` is passed as an `out` parameter. |
|||
|
|||
|
|||
## [0.5.0] - 2020-10-01 |
|||
### New features |
|||
### Changes |
|||
### Fixes |
|||
* Fixed display of ipv6 addresses as strings |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.4.1] - 2020-09-10 |
|||
### New features |
|||
* Added `NetworkDriver.GetEventQueueSizeForConnection` which allows you to check how many pending events a connection has. |
|||
|
|||
### Changes |
|||
### Fixes |
|||
* Fixed a compatibility isue with DOTS Runtime. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.4.0-preview.3] - 2020-08-21 |
|||
### New features |
|||
* Added a new fragmentation pipeline which allows you to send messages larger than one MTU. If the `FragmentationPipelineStage` is part of the pipeline you are trying to send with the `NetworkDriver` will allow a `requiredPayloadSize` larger than one MTU to be specified and split the message into multiple packages. |
|||
|
|||
### Changes |
|||
* The methods to read and write strings in the `DataStreamReader`/`DataStreamWriter` have been changed to use `FixedString<N>` instead of `NativeString<N>`. The name of the methods have also changed from `ReadString` to `ReadFixedString64` - and similar changes for write and the packed version of the calls. The calls support `FixedString32`, `FixedString64`, `FixedString128`, `FixedString512` and `FixedString4096`. |
|||
* Minimum required Unity version has changed to 2020.1.2. |
|||
|
|||
### Fixes |
|||
### Upgrade guide |
|||
The data stream methods for reading and writing strings have changed, they now take `FixedString64` instead of `NativeString64` and the names have changed as follows: |
|||
|
|||
* `DataStreamReader.ReadString` -> `DataStreamReader.ReadFixedString64` |
|||
* `DataStreamReader.ReadPackedStringDelta` -> `DataStreamReader.ReadPackedFixedString64Delta` |
|||
* `DataStreamWriter.WriteString` -> `DataStreamWriter.WriteFixedString64` |
|||
* `DataStreamWriter.WritePackedStringDelta` -> `DataStreamWriter.WritePackedFixedString64Delta` |
|||
|
|||
The transport now requires Unity 2020.1.2. |
|||
|
|||
## [0.3.1-preview.4] - 2020-06-05 |
|||
### New features |
|||
### Changes |
|||
* Added a new `requiredPayloadSize` parameter to `BeginSend`. The required size cannot be larger than `NetworkParameterConstants.MTU`. |
|||
* Added errorcode parameter to a `network_set_nonblocking`, `network_set_send_buffer_size` and `network_set_receive_buffer_size` in `NativeBindings`. |
|||
* Additional APIs added to `NativeBindings`: `network_set_blocking`, `network_get_send_buffer_size`, `network_get_receive_buffer_size`, `network_set_receive_timeout`, `network_set_send_timeout`. |
|||
* Implemented `NetworkEndPoint.AddressAsString`. |
|||
|
|||
### Fixes |
|||
* Fixed an issue in the reliable pipeline which would cause it to not recover if one end did not receive packages for a while. |
|||
* Fixed `NetworkInterfaceEndPoint` and `NetworkEndPoint` `GetHashCode` implementation. |
|||
* Fixed invalid use of strings when specifying the size of socket buffers in the native bindings. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.3.0-preview.6] - 2020-02-24 |
|||
### New features |
|||
### Changes |
|||
* Pipelines are now registered by calling `NetworkPipelineStageCollection.RegisterPipelineStage` before creating a `NetworkDriver`. The built-in pipelines do not require explicit registration. The interface for implementing pipelines has been changed to support this. |
|||
* NetworkDriver is no longer a generic type. You pass it an interface when creating the `NetworkDriver`, which means you can switch between backends without modifying all usage of the driver. There is a new `NetworkDriver.Create` which creates a driver with the default `NetworkInterface`. It is also possible to create a `new NetworkDriver` by passing a `NetworkInterface` instance as the first argument. |
|||
* `NetworkDriver.Send` is replaced by `BeginSend` and `EndSend`. This allows us to do less data copying when sending messages. The interface for implementing new network interfaces has been changed to support this. |
|||
* `DataStreamReader` and `DataStreamWriter` no longer owns any memory. They are just reading/writing the data of a `NativeArray<byte>`. |
|||
* `DataStreamWriter` has explicit types for all Write methods. |
|||
* `DataStreamReader.Context` has been removed. |
|||
* Error handling for `DataStreamWriter` has been improved, on failure it returns false and sets `DataStreamWriter.HasFailedWrites` to true. `DataStreamReader` returns a default value and sets `DataStreamReader.HasFailedReads` to true. `DataStreamReader` will throw an exception instead of returning a default value in the editor. |
|||
* IPCManager is no longer public, it is still possible to create a `NetworkDriver` with a `IPCNetworkInterface`. |
|||
* Added `NetworkDriver.ScheduleFlushSend` which must be called to guarantee that messages are send before next call to `NetworkDriver.ScheduleUpdate`. |
|||
* Added `NetworkDriver.LastUpdateTime` to get the update time the `NetworkDriver` used for the most recent update. |
|||
* Removed the IPC address family, use a IPv4 localhost address instead. |
|||
|
|||
### Fixes |
|||
* Fixed a memory overflow in the reliability pipeline. |
|||
* Made the soaker report locale independent. |
|||
|
|||
### Upgrade guide |
|||
Creation and type of `NetworkDriver` has changed, use `NetworkDriver.Create` or pass an instance of a `NetworkInterface` to the `NetworkDriver` constructor. |
|||
|
|||
`NetworkDriver.Send` has been replaced by a pair of `NetworkDriver.BeginSend` and `NetworkDriver.EndSend`. Calling `BeginSend` will return a `DataStreamWriter` to which you write the data. The `DataStreamWriter` is then passed to `EndSend`. |
|||
|
|||
All write calls in `DataStreamWriter` need an explicit type, for example `Write(0)` should be replaced by `WriteInt(0)`. |
|||
|
|||
`DataStreamWriter` no longer shares current position between copies, if you call a method which writes you must pass it by ref for the modifications to apply. |
|||
|
|||
`DataStreamWriter` no longer returns a DeferedWriter, you need to take a copy of the writer at the point you want to make modifications and use the copy to overwrite data later. |
|||
|
|||
`DataStreamWriter` is no longer disposable. If you use the allocating constructor you need to use `Allocator.Temp`, if you pass a `NativeArray<byte>` to the constructor the `NativeArray` owns the memory. |
|||
|
|||
`DataStreamReader.Context` no longer exists, you need to pass the `DataStreamReader` itself by ref if you read in a different function. |
|||
|
|||
The interface for network pipelines has been changed. |
|||
|
|||
The interface for network interfaces has been changed. |
|||
|
|||
## [0.2.3-preview.0] - 2019-12-12 |
|||
### New features |
|||
### Changes |
|||
* Added reading and write methods for NativeString64 to DataStream. |
|||
|
|||
### Fixes |
|||
### Upgrade guide |
|||
|
|||
## [0.2.2-preview.2] - 2019-12-05 |
|||
### New features |
|||
### Changes |
|||
* Added a stress test for parallel sending of data. |
|||
* Upgraded collections to 0.3.0. |
|||
|
|||
### Fixes |
|||
* Fixed a race condition in IPCNetworkInterface. |
|||
* Changed NetworkEventQueue to use UnsafeList to get some type safety. |
|||
* Fixed an out-of-bounds access in the reliable sequenced pipeline. |
|||
* Fixed spelling and broken links in the documentation. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.2.1-preview.1] - 2019-11-28 |
|||
### New features |
|||
### Changes |
|||
### Fixes |
|||
* Added missing bindings for Linux and Android. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.2.0-preview.4] - 2019-11-26 |
|||
### New features |
|||
### Changes |
|||
* Added support for unquantized floats to `DataStream` class. |
|||
* Added `NetworkConfigParameter.maxFrameTimeMS` so you to allow longer frame times when debugging to prevent disconnections due to timeout. |
|||
* Allow "1.1.1.1:1234" strings when parsing the IP string in the NetworkEndPoint class, it will use the port part when it's present. |
|||
* Reliable pipeline now doesn't require parameters passed in (uses default window size of 32) |
|||
* Added Read/Write of ulong to `DataStream`. |
|||
* Made it possible to get connection state from the parallel NetworkDriver. |
|||
* Added `LengthInBits` to the `DataStreamWriter`. |
|||
|
|||
### Fixes |
|||
* Do not push data events to disconnected connections. Fixes an error about resetting the queue with pending messages. |
|||
* Made the endian checks in `DataStream` compatible with latest version of burst. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.1.2-preview.1] - 2019-07-17 |
|||
### New features |
|||
* Added a new *Ping-Multiplay* sample based on the *Ping* sample. |
|||
* Created to be the main sample for demonstrating Multiplay compatibility and best practices (SQP usage, IP binding, etc.). |
|||
* Contains both client and server code. Additional details in readme in `/Assets/Samples/Ping-Multiplay/`. |
|||
* **DedicatedServerConfig**: Added arguments for `-fps` and `-timeout`. |
|||
* **NetworkEndPoint**: Added a `TryParse()` method which returns false if parsing fails |
|||
* Note: The `Parse()` method returns a default IP / Endpoint if parsing fails, but a method that could report failure was needed for the Multiplay sample. |
|||
* **CommandLine**: |
|||
* Added a `HasArgument()` method which returns true if an argument is present. |
|||
* Added a `PrintArgsToLog()` method which is a simple way to print launch args to logs. |
|||
* Added a `TryUpdateVariableWithArgValue()` method which updates a ref var only if an arg was found and successfully parsed. |
|||
|
|||
### Changes |
|||
* Deleted existing SQP code and added reference to SQP Package (now in staging). |
|||
* Removed SQP server usage from basic *Ping* sample. |
|||
* Note: The SQP server was only needed for Multiplay compatibility, so the addition of *Ping-Multiplay* allowed us to remove SQP from *Ping*. |
|||
|
|||
### Fixes |
|||
* **DedicatedServerConfig**: Vsync is now disabled programmatically if requesting an FPS different from the current screen refresh rate. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.1.1-preview.1] - 2019-06-05 |
|||
### New features |
|||
* Moved MatchMaking to a package and supporting code to a separate folder. |
|||
|
|||
### Fixes |
|||
* Fixed an issue with the reliable pipeline not resending when completely idle. |
|||
|
|||
### Upgrade guide |
|||
|
|||
## [0.1.0-preview.1] - 2019-04-16 |
|||
### New features |
|||
* Added network pipelines to enable processing of outgoing and incomming packets. The available pipeline stages are `ReliableSequencedPipelineStage` for reliable UDP messages and `SimulatorPipelineStage` for emulating network conditions such as high latency and packet loss. See [the pipeline documentation](com.unity.transport/Documentation~/pipelines-usage.md) for more information. |
|||
* Added reading and writing of packed signed and unsigned integers to `DataStream`. These new methods use huffman encoding to reduce the size of transfered data for small numbers. |
|||
|
|||
### Changes |
|||
* Enable Burst compilation for most jobs. |
|||
* Made it possible to get the remote endpoint for a connection. |
|||
* Replacing EndPoint parsing with custom code to avoid having a dependency on `System.Net`. |
|||
* Change the ping sample command-line parameters for server to `-port` and `-query_port`. |
|||
* For matchmaking, use an Assignment object containing the `ConnectionString`, the `Roster`, and an `AssignmentError` string instead of just the `ConnectionString`. |
|||
|
|||
### Fixes |
|||
* Fixed an issue with building iOS on Windows. |
|||
* Fixed inconsistent error handling between platforms when the network buffer is full. |
|||
|
|||
### Upgrade guide |
|||
Unity 2019.1 is now required. |
|||
|
|||
`BasicNetworkDriver` has been renamed to `GenericNetworkDriver` and a new `UdpNetworkDriver` helper class is also available. |
|||
|
|||
`System.Net` EndPoints can no longer be used as addresses, use the new NetworkEndpoint struct instead. |
|
|||
fileFormatVersion: 2 |
|||
guid: 2f10a9a833f64e641a5733d67911a362 |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
# Unity Transport Design Rules |
|||
|
|||
## All features are optional |
|||
Unity transport is conceptually a thin layer on UDP adding a connection concept. All additional features on top of UDP + connection are optional, when not used they have zero performance or complexity overhead. If possible features are implemented as pipeline stages. |
|||
|
|||
Features that have a limited audience are implemented outside the package - either in game code or other packages. |
|||
|
|||
## Full control over processing time and when packets are sent/received |
|||
UTP is optimized for making games. It can be used without creating any additional threads - only using the JobSystem. The layer on top has full control over when the transport schedules jobs. The layer on top also has full control over when packets are sent on the wire. There are no internal buffers delaying messages (except possibly in pipelines). |
|||
|
|||
There is generally no need to continuously poll for messages since incoming data needs to be read right before simulation starts, and we cannot start using new data in the middle of the simulation |
|||
|
|||
## Written in HPC# |
|||
All code is jobified and burst compiled, there is no garbage collection. The transport does not spend any processing time outside setup on the main thread, and it allows the layer on top to not sync on the main thread. |
|||
|
|||
## Follows the DOTS principles, is usable in DOTS Runtime and always compatible with the latest versions of the DOTS packages |
|||
There should always be a version compatible with the latest verions of the DOTS dependencies such as Unity Collections. |
|||
|
|||
## The protocol is well defined and documented |
|||
Other implementations can communicate with games written with Unity Transport, without reverse engineering or reading the transport source code |
|
|||
fileFormatVersion: 2 |
|||
guid: 99b333ad37d614e42b1a4776de09e34e |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
# Unity Transport manual |
|||
|
|||
* **Introduction** |
|||
* [Unity Transport overview](index.md) |
|||
* [Installation guide](install.md) |
|||
* **Workflows** |
|||
* [Creating a minimal client and server](workflow-client-server.md) |
|||
* [Creating a jobified client and server](workflow-client-server-jobs.md) |
|||
* [Using pipelines](pipelines-usage.md) |
|||
* **Background information** |
|||
* [Understanding the Update flow](update-flow.md) |
|||
* [Understanding the Connection State Machine](connection-state-machine.md) |
|||
* [Event consumption](event-consumption.md) |
|||
* **Samples** |
|||
* [ClientBehaviour](samples/clientbehaviour.cs.md) |
|||
* [ServerBehaviour](samples/serverbehaviour.cs.md) |
|||
* [JobifiedClientBehaviour](samples/jobifiedclientbehaviour.cs.md) |
|||
* [JobifiedServerBehaviour](samples/jobifiedserverbehaviour.cs.md) |
|||
* [Source Project for Workflows](https://oc.unity3d.com/index.php/s/PHaNZP79Va2YOLT) |
|||
|
|
|||
# Understanding the Connection State Machine |
|||
|
|||
It's important to at least understand how transitions occur in the connection state machine so you make decisions depending on what triggered each state. And to understand the subtle differences depending if you are `Connecting` to another host or if you simply want to Listen for incoming connections. As you can see below the state machine for the `NetworkConnection` is pretty simple. |
|||
|
|||
![ConnectionState](images/com.unity.transport.connection.png) |
|||
|
|||
All connections start in `Disconnected` state. |
|||
|
|||
- Depending what state the `NetworkDriver` is in, the `Listening (Passive)` state might be triggered. This is when the driver acts like a server listening for incoming connections and data requests. And secondly you could try to use the driver to connect to a remote endpoint and then we would invoke another flow of the state machine. |
|||
|
|||
So to give a overview we have two standard scenarios. Either you listen for incoming connections or you use and outgoing connection to connect to someone else. |
|||
|
|||
In our [client/server workflow](workflow-client-server.md) we use the ServerBehaviour to `Listen` and the ClientBehaviour to `Connect`. |
|||
|
|||
|
|||
|
|||
[Back to table of contents](TableOfContents.md) |
|
|||
# Event consumption |
|||
|
|||
There are currently 4 types of events supplied by the `NetworkDriver` |
|||
|
|||
```c# |
|||
public enum Type |
|||
{ |
|||
Empty = 0, |
|||
Data, |
|||
Connect, |
|||
Disconnect |
|||
} |
|||
``` |
|||
|
|||
As mentioned, there are a few subtle differences running the driver as a host or client. Mainly when it comes to consumption of events. |
|||
|
|||
Both your client and your server loop will want to consume the events that are produced by the `NetworkDriver`. And you do so by either calling `PopEvent` on each `NetworkConnection` similar to how we did before. |
|||
|
|||
```c# |
|||
DataStreamReader strm; |
|||
NetworkEvent.Type cmd; |
|||
while ((cmd = m_Connection.PopEvent(driver, out strm)) != NetworkEvent.Type.Empty) |
|||
; // Handle Event |
|||
``` |
|||
|
|||
You can try calling the `PopEventForConnection` on the `NetworkDriver` as we did in the ServerBehaviour example: |
|||
|
|||
```c# |
|||
DataStreamReader strm; |
|||
NetworkEvent.Type cmd; |
|||
while ((cmd = m_Driver.PopEventForConnection(m_Connections[i], out strm)) != NetworkEvent.Type.Empty) |
|||
; // Handle Event |
|||
``` |
|||
|
|||
There is no real difference between these calls, both calls will do the same thing. Its just how you want to phrase yourself when writing the code. |
|||
|
|||
And finally to receive a new `NetworkConnection` on the Driver while Listening you can call `Accept` |
|||
|
|||
```c# |
|||
NetworkConnection c; |
|||
while ((c = m_Driver.Accept()) != default(NetworkConnection)) |
|||
; // Handle Connection Event. |
|||
``` |
|||
|
|||
| Event | Description | |
|||
| ---------- | ------------------------------------------------------------ | |
|||
| Empty | The `Empty` event signals that there are no more messages in our event queue to handle this frame. | |
|||
| Data | The `Data` event signals that we have received data from a connected endpoint. | |
|||
| Connect | The `Connect` event signals that a new connection has been established.<br> **Note**: this event is only available if the `NetworkDriver` is **not** in the `Listening` state. | |
|||
| Disconnect | The `Disconnect` event is received if;<br> 1. `Disconnect` packet was received (calling `NetworkConnection::Disconnect` will trigger this.)<br> 2. A *socket timeout* occurred.<br> 3. Maximum connect attempts on the `NetworkConnection` exceeded. <br> **Note:** That if you call `Disconnect` on your `NetworkConnection` this will **NOT** trigger an `Disconnect` event on your local `NetworkDriver`. | |
|||
|
|||
Looking at this table we see that there are 2 things that stand out. |
|||
|
|||
- The first thing is that the `Connect` event is only available if the `NetworkDriver` is **NOT** `Listening` |
|||
- In order to receive any `Connect` events on a `NetworkDriver` that is in the `Listening` state we need to call the special function `Accept` just as we did in the *Creating a Server* section in the [Creating a minimal client and server](workflow-client-server.md) workflow page. |
|||
- The second thing to notice is that if you call `Disconnect` on a `NetworkConnection` this will not trigger an event inside your own driver. |
|||
|
|||
|
|||
|
|||
[Back to table of contents](TableOfContents.md) |
部分文件因为文件数量过多而无法显示
撰写
预览
正在加载...
取消
保存
Reference in new issue