您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
430 行
18 KiB
430 行
18 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Newtonsoft.Json;
|
|
using UnityEngine;
|
|
using Unity.Services.Authentication;
|
|
using Unity.Services.Core;
|
|
using Unity.Services.Lobbies;
|
|
using Unity.Services.Lobbies.Http;
|
|
using Unity.Services.Lobbies.Models;
|
|
using Random = UnityEngine.Random;
|
|
|
|
public class LobbyHelloWorld : MonoBehaviour
|
|
{
|
|
// Inspector properties with initial values
|
|
public string newLobbyName = Guid.NewGuid().ToString();
|
|
public int maxPlayers = 8;
|
|
public bool isPrivate = false;
|
|
|
|
// We'll only be in one lobby at once for this demo, so let's track it here
|
|
private Lobby currentLobby;
|
|
|
|
async void Start()
|
|
{
|
|
try
|
|
{
|
|
await ExecuteLobbyDemoAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogException(ex);
|
|
}
|
|
finally
|
|
{
|
|
await CleanupDemoLobbyAsync();
|
|
}
|
|
}
|
|
|
|
// Clean up the lobby we're in if we're the host
|
|
async Task CleanupDemoLobbyAsync()
|
|
{
|
|
var localPlayerId = AuthenticationService.Instance.PlayerId;
|
|
|
|
// This is so that orphan lobbies aren't left around in case the demo fails partway through
|
|
if (currentLobby != null && currentLobby.HostId.Equals(localPlayerId))
|
|
{
|
|
await DeleteLobbyAsync(currentLobby.Id);
|
|
}
|
|
}
|
|
|
|
// A basic demo of lobby functionality
|
|
async Task ExecuteLobbyDemoAsync()
|
|
{
|
|
await UnityServices.Initialize();
|
|
|
|
// Log in a player for this game client
|
|
Player loggedInPlayer = await GetPlayerFromAnonymousLoginAsync();
|
|
|
|
// Add some data to our player
|
|
// This data will be included in a lobby under players -> player.data
|
|
loggedInPlayer.Data.Add("Ready", new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "No"));
|
|
|
|
// Look for existing lobbies
|
|
List<Lobby> foundLobbies = await QueryLobbiesAsync(new QueryRequest(
|
|
count: 10, // Return up to 10 results
|
|
sampleResults: false, // Do not give random results
|
|
filter: new List<QueryFilter>
|
|
{
|
|
// Use filters to only return lobbies which match specific conditions
|
|
// You can only filter on built-in properties (Ex: AvailableSlots) or indexed custom data (S1, N1, etc.)
|
|
// Take a look at the API for other built-in fields you can filter on
|
|
|
|
// Let's search for games with open slots (AvailableSlots greater than 0)
|
|
new QueryFilter(
|
|
field: QueryFilter.FieldOptions.AvailableSlots,
|
|
op: QueryFilter.OpOptions.GT,
|
|
value: "0"),
|
|
|
|
// Let's add some filters for custom indexed fields
|
|
new QueryFilter(
|
|
field: QueryFilter.FieldOptions.S1, // S1 = "Test"
|
|
op: QueryFilter.OpOptions.EQ,
|
|
value: "true"),
|
|
|
|
new QueryFilter(
|
|
field: QueryFilter.FieldOptions.S2, // S2 = "GameMode"
|
|
op: QueryFilter.OpOptions.EQ,
|
|
value: "ctf"),
|
|
|
|
// Example "skill" range filter (skill is a custom numeric field in this example)
|
|
new QueryFilter(
|
|
field: QueryFilter.FieldOptions.N1, // N1 = "Skill"
|
|
op: QueryFilter.OpOptions.GT,
|
|
value: "0"),
|
|
|
|
new QueryFilter(
|
|
field: QueryFilter.FieldOptions.N1, // N1 = "Skill"
|
|
op: QueryFilter.OpOptions.LT,
|
|
value: "51"),
|
|
},
|
|
order: new List<QueryOrder>
|
|
{
|
|
// Order results by available player slots (least first), then by lobby age, then by lobby name
|
|
new QueryOrder(true, QueryOrder.FieldOptions.AvailableSlots),
|
|
new QueryOrder(false, QueryOrder.FieldOptions.Created),
|
|
new QueryOrder(false, QueryOrder.FieldOptions.Name),
|
|
}));
|
|
|
|
if (foundLobbies.Any()) // Try to join a random lobby if one exists
|
|
{
|
|
// Let's print info about the lobbies we found
|
|
Debug.Log("Found lobbies:\n" + JsonConvert.SerializeObject(foundLobbies));
|
|
|
|
// Let's pick a random lobby to join
|
|
var randomLobby = foundLobbies[Random.Range(0, foundLobbies.Count)];
|
|
|
|
// Try to join the lobby
|
|
// Player is optional because the service can pull the player data from the auth token
|
|
currentLobby = await JoinLobbyAsync(lobbyId: randomLobby.Id, player: null);
|
|
|
|
// You can also join via a Lobby Code instead of a lobby ID
|
|
// Lobby Codes are a short, unique codes that map to a specific lobby Id
|
|
// currentLobby = await JoinLobbyWithCodeAsync("MyLobbyCode");
|
|
}
|
|
else // Didn't find any lobbies, create a new lobby
|
|
{
|
|
// Populate the new lobby with some data; use indexes so it's easy to search for
|
|
var lobbyData = new Dictionary<string, DataObject>()
|
|
{
|
|
["Test"] = new DataObject(DataObject.VisibilityOptions.Public, "true", DataObject.IndexOptions.S1),
|
|
["GameMode"] = new DataObject(DataObject.VisibilityOptions.Public, "ctf", DataObject.IndexOptions.S2),
|
|
["Skill"] = new DataObject(DataObject.VisibilityOptions.Public, Random.Range(1, 51).ToString(), DataObject.IndexOptions.N1),
|
|
["Rank"] = new DataObject(DataObject.VisibilityOptions.Public, Random.Range(1, 51).ToString()),
|
|
};
|
|
|
|
// Create a new lobby
|
|
currentLobby = await CreateLobbyAsync(new CreateRequest(
|
|
name: newLobbyName,
|
|
player: loggedInPlayer,
|
|
maxPlayers: maxPlayers,
|
|
isPrivate: isPrivate,
|
|
data: lobbyData));
|
|
}
|
|
|
|
// Let's write a little info about the lobby we joined / created
|
|
Debug.Log("Lobby info:\n" + JsonConvert.SerializeObject(currentLobby));
|
|
|
|
// Let's add some new data for our player and update the lobby state
|
|
// Players can update their own data
|
|
loggedInPlayer.Data.Add("ExamplePublicPlayerData",
|
|
new PlayerDataObject(PlayerDataObject.VisibilityOptions.Public, "Everyone can see this"));
|
|
|
|
loggedInPlayer.Data.Add("ExamplePrivatePlayerData",
|
|
new PlayerDataObject(PlayerDataObject.VisibilityOptions.Private, "Only the host sees this"));
|
|
|
|
loggedInPlayer.Data.Add("ExampleMemberPlayerData",
|
|
new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "Only lobby members see this"));
|
|
|
|
// Update the lobby
|
|
currentLobby = await UpdateLobbyPlayerDataAsync(currentLobby.Id, loggedInPlayer);
|
|
Debug.Log("Updated lobby info:\n" + JsonConvert.SerializeObject(currentLobby));
|
|
|
|
// Let's poll for the lobby data again just to see what it looks like
|
|
currentLobby = await GetLobbyAsync(currentLobby.Id);
|
|
Debug.Log("Updated lobby info:\n" + JsonConvert.SerializeObject(currentLobby));
|
|
|
|
if (!currentLobby.HostId.Equals(loggedInPlayer.Id))
|
|
{
|
|
// Since we're not the lobby host, let's just leave the lobby
|
|
await RemovePlayerFromLobbyAsync(currentLobby.Id, loggedInPlayer.Id);
|
|
}
|
|
else
|
|
{
|
|
// Only hosts can set lobby data, and we're the host, so let's set some
|
|
// Note that lobby host can be passed around intentionally (by the current host updating the host id)
|
|
// Host is randomly assigned if the previous host leaves
|
|
|
|
// Let's update some existing lobby data
|
|
currentLobby.Data["GameMode"] =
|
|
new DataObject(DataObject.VisibilityOptions.Public, "deathmatch", DataObject.IndexOptions.S2);
|
|
|
|
// Let's add some new data to the lobby
|
|
currentLobby.Data.Add("ExamplePublicLobbyData",
|
|
new DataObject(DataObject.VisibilityOptions.Public, "Everyone can see this"));
|
|
|
|
currentLobby.Data.Add("ExamplePrivateLobbyData",
|
|
new DataObject(DataObject.VisibilityOptions.Private, "Only the host sees this"));
|
|
|
|
currentLobby.Data.Add("ExampleMemberLobbyData",
|
|
new DataObject(DataObject.VisibilityOptions.Member, "Only lobby members see this"));
|
|
|
|
// OK, now let's try to push these local changes to the service
|
|
currentLobby = await UpdateLobbyDataAsync(currentLobby);
|
|
|
|
// Let's print the updated lobby
|
|
Debug.Log("Updated lobby info:\n" + JsonConvert.SerializeObject(currentLobby));
|
|
|
|
// OK, we're done with the lobby - let's delete it
|
|
await DeleteLobbyAsync(currentLobby.Id);
|
|
}
|
|
|
|
// Now, let's try the QuickJoin API, which just puts our player in a matching lobby automatically
|
|
// This is fast and reliable, but removes some user interactivity (can't choose from a list of lobbies)
|
|
// You can use filters to specify which types of lobbies can be joined
|
|
currentLobby = await QuickJoinLobbyAsync(loggedInPlayer, new List<QueryFilter>
|
|
{
|
|
// Let's search for games with open slots (AvailableSlots greater than 0)
|
|
new QueryFilter(
|
|
field: QueryFilter.FieldOptions.AvailableSlots,
|
|
op: QueryFilter.OpOptions.GT,
|
|
value: "0"),
|
|
|
|
// You can add more filters here, such as filters on custom data fields
|
|
});
|
|
|
|
// If we didn't find a lobby, abort run
|
|
if (currentLobby == null)
|
|
{
|
|
Debug.Log("Unable to find a lobby using Query and Join");
|
|
return;
|
|
}
|
|
|
|
// Let's write a little info about the lobby we quick-joined
|
|
Debug.Log("Lobby info:\n" + JsonConvert.SerializeObject(currentLobby));
|
|
|
|
// There's not anything else we can really do here, so let's leave the lobby
|
|
await RemovePlayerFromLobbyAsync(currentLobby.Id, loggedInPlayer.Id);
|
|
}
|
|
|
|
// Log in a player using Unity's "Anonymous Login" API and construct a Player object for use with the Lobbies APIs
|
|
static async Task<Player> GetPlayerFromAnonymousLoginAsync()
|
|
{
|
|
if (!AuthenticationService.Instance.IsSignedIn)
|
|
{
|
|
Debug.Log($"Trying to log in a player ...");
|
|
|
|
// Use Unity Authentication to log in
|
|
await AuthenticationService.Instance.SignInAnonymouslyAsync();
|
|
|
|
if (!AuthenticationService.Instance.IsSignedIn)
|
|
{
|
|
Debug.Log("Player was not signed in successfully?");
|
|
throw new InvalidOperationException("Unable to continue with no logged in player");
|
|
}
|
|
}
|
|
|
|
Debug.Log("Player signed in as " + AuthenticationService.Instance.PlayerId);
|
|
|
|
// Player objects have Get-only properties, so you need to initialize the data bag here if you want to use it
|
|
return new Player(AuthenticationService.Instance.PlayerId, null, new Dictionary<string, PlayerDataObject>());
|
|
}
|
|
|
|
// Create a new lobby
|
|
static async Task<Lobby> CreateLobbyAsync(CreateRequest createRequest)
|
|
{
|
|
Debug.Log($"Trying to create a new lobby ...");
|
|
Response<Lobby> response = await LobbyService.LobbyApiClient.CreateLobbyAsync(new CreateLobbyRequest(createRequest));
|
|
Debug.Log($"Player {createRequest.Player.Id} created lobby {response.Result.Id}");
|
|
return response.Result;
|
|
}
|
|
|
|
// Get a list of existing lobbies (Optional: use a filter)
|
|
static async Task<List<Lobby>> QueryLobbiesAsync(QueryRequest queryRequest)
|
|
{
|
|
Debug.Log($"Trying to query compatible lobbies ...");
|
|
Response<QueryResponse> response = await LobbyService.LobbyApiClient.QueryLobbiesAsync(new QueryLobbiesRequest(queryRequest));
|
|
Debug.Log($"Found {response.Result.Results.Count} matching lobbies");
|
|
return response.Result.Results;
|
|
}
|
|
|
|
// Ask the service to join any available lobby; can provide a filter
|
|
static async Task<Lobby> QuickJoinLobbyAsync(Player player, List<QueryFilter> filter = null)
|
|
{
|
|
Debug.Log($"Trying to query and join a compatible lobby ...");
|
|
|
|
var request = new QuickJoinLobbyRequest(new QuickJoinRequest(
|
|
player: player,
|
|
filter: filter
|
|
));
|
|
|
|
// This is an example of how to handle some common errors when calling the service
|
|
// 404s are expected if no lobby is found, and the API currently throws exceptions on 404
|
|
try
|
|
{
|
|
Response<Lobby> response = await LobbyService.LobbyApiClient.QuickJoinLobbyAsync(request);
|
|
Debug.Log($"Player {player.Id} joined lobby {response.Result.Id}");
|
|
return response.Result;
|
|
}
|
|
catch (HttpException ex)
|
|
{
|
|
if (TryGetErrorFromResponse(ex.Response, out var error))
|
|
{
|
|
// If we got a structured error we can deserialize, let's log it
|
|
Debug.Log($"Lobby service error; Http Status {ex.Response.StatusCode}" +
|
|
$"\nResponse Error: {ex.Response.ErrorMessage}" +
|
|
$"\nLobby Error Status: {error.status}" +
|
|
$"\nLobby Error Title: {error.title}");
|
|
}
|
|
else
|
|
{
|
|
// For error we can't deserialize, log the exception + body for debugging
|
|
Debug.Log($"Lobby service error; Http Status {ex.Response.StatusCode}" +
|
|
$"\nResponse Error: {ex.Response.ErrorMessage}" +
|
|
$"\nBody: {Encoding.UTF8.GetString(ex.Response.Data)}");
|
|
|
|
Debug.LogException(ex);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Try to join a specific lobby
|
|
static async Task<Lobby> JoinLobbyAsync(string lobbyId, Player player = null)
|
|
{
|
|
Debug.Log($"Attempting to join lobby {lobbyId} ...");
|
|
Response<Lobby> response = await LobbyService.LobbyApiClient.JoinLobbyByIdAsync(
|
|
new JoinLobbyByIdRequest(lobbyId, player));
|
|
Debug.Log($"Joined lobby {response.Result.Id}");
|
|
return response.Result;
|
|
}
|
|
|
|
// Try to join a specific lobby; use a lobbyCode to join instead of a LobbyId
|
|
static async Task<Lobby> JoinLobbyWithCodeAsync(string lobbyCode, Player player = null)
|
|
{
|
|
Debug.Log($"Attempting to join lobby with lobby code {lobbyCode} ...");
|
|
Response<Lobby> response = await LobbyService.LobbyApiClient.JoinLobbyByCodeAsync(
|
|
new JoinLobbyByCodeRequest(new JoinByCodeRequest(lobbyCode, player)));
|
|
Debug.Log($"Joined lobby {response.Result.Id}");
|
|
return response.Result;
|
|
}
|
|
|
|
// Get details about a specific lobby
|
|
static async Task<Lobby> GetLobbyAsync(string lobbyId)
|
|
{
|
|
Debug.Log($"Getting lobby {lobbyId} ...");
|
|
Response<Lobby> response = await LobbyService.LobbyApiClient.GetLobbyAsync(
|
|
new GetLobbyRequest(lobbyId));
|
|
return response.Result;
|
|
}
|
|
|
|
// Delete an existing lobby; see API docs for access restrictions
|
|
static async Task DeleteLobbyAsync(string lobbyId)
|
|
{
|
|
Debug.Log($"Deleting lobby {lobbyId} ...");
|
|
Response response = await LobbyService.LobbyApiClient.DeleteLobbyAsync(
|
|
new DeleteLobbyRequest(lobbyId));
|
|
Debug.Log($"Lobby {lobbyId} deleted");
|
|
}
|
|
|
|
// Remove a player from a lobby; see API docs for access restrictions
|
|
static async Task RemovePlayerFromLobbyAsync(string lobbyId, string playerId)
|
|
{
|
|
Debug.Log($"Removing {playerId} from lobby {lobbyId} ...");
|
|
var request = new RemovePlayerRequest(lobbyId, playerId);
|
|
Response response = await LobbyService.LobbyApiClient.RemovePlayerAsync(request);
|
|
Debug.Log($"Player {playerId} removed from lobby {lobbyId}");
|
|
}
|
|
|
|
// Set lobby data; see API docs for access restrictions
|
|
static async Task<Lobby> UpdateLobbyDataAsync(Lobby lobby)
|
|
{
|
|
Debug.Log($"Updating lobby data for lobby {lobby.Id} ...");
|
|
|
|
UpdateRequest updatedState = new UpdateRequest(
|
|
maxPlayers: lobby.MaxPlayers,
|
|
isPrivate: lobby.IsPrivate,
|
|
data: lobby.Data,
|
|
hostId: lobby.HostId,
|
|
name: lobby.Name);
|
|
|
|
var updateReq = new UpdateLobbyRequest(lobby.Id, updatedState);
|
|
Response<Lobby> response = await LobbyService.LobbyApiClient.UpdateLobbyAsync(updateReq);
|
|
Debug.Log($"Lobby data for lobby {lobby.Id} updated");
|
|
return response.Result;
|
|
}
|
|
|
|
// Set player data in a lobby; see API docs for access restrictions
|
|
static async Task<Lobby> UpdateLobbyPlayerDataAsync(string lobbyId, Player player)
|
|
{
|
|
Debug.Log($"Updating player data for player {player.Id} in lobby {lobbyId} ...");
|
|
|
|
var newPlayerState = new PlayerUpdateRequest(
|
|
connectionInfo: player.ConnectionInfo,
|
|
data: player.Data,
|
|
allocationId: player.AllocationId);
|
|
|
|
var updateReq = new UpdatePlayerRequest(lobbyId, player.Id, newPlayerState);
|
|
|
|
Response<Lobby> response = await LobbyService.LobbyApiClient.UpdatePlayerAsync(updateReq);
|
|
Debug.Log($"Player data for player {player.Id} in lobby {lobbyId} updated");
|
|
return response.Result;
|
|
}
|
|
|
|
// A class for errors returned from the service on failures (4xx errors, etc.)
|
|
class LobbyError
|
|
{
|
|
public int status { get; set; }
|
|
public string title { get; set; }
|
|
public Detail[] details { get; set; }
|
|
}
|
|
|
|
class Detail
|
|
{
|
|
public string message { get; set; }
|
|
public string errorType { get; set; }
|
|
}
|
|
|
|
static bool TryGetErrorFromResponse(HttpClientResponse response, out LobbyError error)
|
|
{
|
|
var decodedJsonString = Encoding.UTF8.GetString(response.Data);
|
|
|
|
var success = true;
|
|
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
Error = (sender, args) =>
|
|
{
|
|
success = false;
|
|
args.ErrorContext.Handled = true;
|
|
},
|
|
MissingMemberHandling = MissingMemberHandling.Ignore
|
|
};
|
|
|
|
error = JsonConvert.DeserializeObject<LobbyError>(decodedJsonString, settings);
|
|
return success;
|
|
}
|
|
}
|