Matchmaking API
概述
建立多人遊戲的一個關鍵要求是能夠輕鬆地將具有相似技能、級別或想要玩相同遊戲類型或地圖的玩家配對在一起,使遊戲中的整體體驗盡可能愉快。
為此,Photon Fusion公開了一組API調用,可用於為尋找完美配對的玩家創造最佳體驗。
Photon Fusion與Photon Cloud透明地協同工作,因此與Photon後端服務的大多數互動都是在內部自動完成的。
在本頁中,介紹了Fusion Matchmaking API,該API用於建立具有可選自訂屬性的Game Session,玩家可以使用該自訂屬性根據其期望的遊戲體驗來篩選/加入最佳Session。
術語
為了充分理解API,以下描述了本檔案中使用的與Matchmaking API的正確使用直接相關的一些術語。
- Game Session:或簡稱為- Session,是玩家見面進行比賽或交流的地方。這是在Photon Cloud中發佈的,可以讓其他客戶端搜尋、篩選和加入特定遊戲。任何在- Session之外的通信都是不可能的,任何客戶端只能在一個- Session中處於活躍中狀態。- Game Sessions具有以下屬性和方法:可以按名稱建立和加入,- Custom Properties,具有最大玩家數量,可以隱藏(不顯示在- Lobby中)或可見,可以關閉(沒有人可以加入)或打開。在- PUN和- Photon Realtime中,它以前被稱為- Room。
- Lobby:是- Sessions的虛擬容器或「清單」。例如,可以使用多個大廳將遊戲階段分為不同的遊戲類型,因為這基本上是一種在任意一組遊戲階段中列出- Game Session的方法。客戶端 無法 在- Lobby進行通信,他們永遠不知道大廳裏還有另一個客戶端。客戶只能在- Lobby、- Game Session或不在兩者中。
建立和加入遊戲階段
Game Session的建立和加入是同一過程的兩個部分,規則很簡單:
1. 如果沒有具有指定SessionName的Session,則將建立一個具有該SessionName的新遊戲階段(並非在所有情況下,如下文所述)。
2. 如果已經存在具有該SessionName的遊戲階段,則同儕節點將加入它。
就API而言,所有這些都是在啟動新的Fusion模擬時自動完成的。
下方列出了在建立新遊戲階段時,可用於自訂Session或在查找要加入的Session時用作篩選器的主要引數:
C#
NetworkRunner.StartGame(new StartGameArgs {
  // ...
  // Arguments related to the Matchmaking between peers
  SessionName = [string],
  SessionProperties = [Dictionary<string, SessionProperty>],
  CustomLobbyName = [string],
  EnableClientSessionCreation = [bool],
  PlayerCount = [int],
  IsOpen = [bool],
  IsVisible = [bool],
  MatchmakingMode = [MatchmakingMode],
});
所有與對戰配對相關的引數都是可選的,每個引數的預設值如下所述:
- SessionName:Game Session的Name或ID,它將標識Photon Cloud上的遊戲階段,並且它在地區內 必須 是唯一的。如果沒有設定名稱,Fusion將生成一個隨機的GUID來標識遊戲階段。
- SessionProperties:Session的Custom Properties是在Game Session中包含中繼資料的方式,例如遊戲模式/類型或當前地圖。請記住,在創建Session時,所有屬性都會發佈到Lobby,當同儕節點加入隨機的Session時,這些屬性可以用作配對篩選器(下方閱讀更多)。建議是,始終儘量保持Property Keys盡可能短,以儘量減少流量。預設下,Session Custom Properties為空,不包含任何額外資訊。
- CustomLobbyName:此引數用於設定與Session關聯的自訂Lobby Name。預設下,Fusion已經根據GameMode(在Host、Server或Client中啟動時為ClientServer Lobby,在Shared遊戲模式中啟動時,為Shared Lobby)分離了一個Session。
- EnableClientSessionCreation:此旗標更改哪些Client同儕節點類型可以建立新的Game Session或只能加入一個。請參閱下文瞭解更多資訊。
- PlayerCount:定義可以加入Session的最大客戶端數。此參數僅在創建新的Session時使用,預設下,它從NetworkProjectConfig/Simulation上的Default Players欄位中獲取值。
- IsOpen:定義正在創建的Game Session(適用時)是否將設定為開放,以便任何其他玩家加入。有關更多資訊,請參閱獲得與更新遊戲階段資訊。
- IsVisible:定義正在創建的Game Session(適用時)是否對其他玩家可見。有關更多資訊,請參閱獲得與更新遊戲階段資訊。
- MatchmakingMode:定義嘗試加入隨機Game Session時使用的對戰配對模式。- FillRoom:填充房間(最舊的先),以讓玩家儘快聚在一起。預設。
- SerialMatching:按順序在可用房間中分配玩家,但會考慮篩選器。沒有篩選器的話,房間裡的玩家將均勻分配。
- RandomMatching:加入(完全)隨機房間。預期的屬性必須匹配,但除此之外,可以選擇任何可用的房間。
 
可以隨機或使用特定的SessionName(例如,對遊戲邀請有用)來創建和加入Game Session,還可以使用自訂屬性啟用Session篩選,以便只加入具有特定設置的遊戲。
這在管理遊戲階段時提供了很大的靈活性。
下表總結了Fusion如何處理Game Session的創建和加入,因為它取決於SessionName、GameMode以及啟動模擬時是否啟用了EnableClientSessionCreation。
| 遊戲模式 | 遊戲階段名稱 | |||||
|---|---|---|---|---|---|---|
| 有效 | 空的/空值 | |||||
| 伺服器/主機 | 建立或加入特定遊戲階段 | 以隨機ID建立或加入遊戲階段 | ||||
| EnableClientSessionCreation | ||||||
| 空值(預設) | 真 | 偽 | 空值(預設) | 真 | 偽 | |
| 客戶端 | 加入遊戲階段 | 建立或加入遊戲階段 | 加入遊戲階段 | 加入隨機遊戲階段 | 加入隨機或建立 | 加入隨機遊戲階段 | 
| 共享 | 建立或加入遊戲階段 | 建立或加入遊戲階段 | 加入遊戲階段 | 加入隨機或建立 | 加入隨機或建立 | 加入隨機遊戲階段 | 
| AutoHostOrClient | 建立或加入遊戲階段 | 建立或加入遊戲階段 | 加入遊戲階段 | 加入隨機或建立 | 加入隨機或建立 | 加入隨機遊戲階段 | 
獲取和更新遊戲階段資訊
Fusion提供了有關當前連線的Game Session的大量資訊,如Name和Region。
這些資料可以透過SessionInfo屬性直接在NetworkRunner中獲得。
下方列出了SessionInfo類型的所有可用欄位:
- IsValid [bool{get}]:如果- SessionInfo已準備好進行讀取/寫入,則發出訊號。
- Name [string{get}]:- Session Name。
- Region [string{get}]:目前已連線的- Region。
- Properties [Dictionary<string, SessionProperty>{get}]:一個具有當前- Session Custom Properties的唯讀字典。為了更新這些屬性,只需使用- SessionInfo.UpdateCustomProperties(Dictionary<string, SessionProperty>)方法,並傳遞一組新的屬性。
- IsVisible [bool{get,set}]:如果- Session在- Lobby上為- Visible,則發出訊號。使- Session不可見,是更改此屬性的事情。
- IsOpen [bool{get,set}]:如果- Session是- Open加入,則發出訊號。要關閉或打開- Sesion,只需更改此屬性。
- PlayerCount [int{get}]:- Session中當前的- Players數量。僅在大廳可用。
- MaxPlayers [int{get}]:可以加入- Session的同儕節點的- Max Number,此值還包括- Server/Host同儕節點的槽。僅在大廳可用。
請記住,例如,Session資訊和主要的Custom Properties應僅用於對戰配對目的,比如 絕不能 與遊戲客戶端同步遊戲狀態資訊。我們強烈反對這種使用。
如果您需要在整個遊戲階段範圍內交換與遊戲遊玩相關的資訊,Fusion提供了很多選擇,比如擁有一個全域的NetworkObject或使用RPC來獲取一次性資料。
NetworkRunner還提供了一些其他與Session和Photon Cloud相關的屬性,可以在遊戲中使用,比如:
- NetworkRunner.IsCloudReady:如果本機同儕節點連線到- Photon Cloud並且能夠建立/加入房間或加入大廳,則發出訊號。
- NetworkRunner.UserId:在本機同儕節點通過身份驗證後,保存與本機同儕節點關聯的- UserId。此資訊來自您的應用程式使用的身份驗證服務。
- NetworkRunner.AuthenticationValues:保存啟動Fusion時用於對本機同儕節點進行身份驗證的- AuthenticationValues的參照。
- NetworkRunner.CurrentConnectionType:描述同儕節點使用的當前連線類型,是與遠端- Server的- Direct或- Relayed連線。請記住,在- SharedMode中,客戶端始終透過中繼連線。
- NetworkRunner.NATType:當啟用NAT穿透系統時,Fusion將嘗試確定本機同儕節點正在運行的當前網路的當前NAT類型,此屬性會公開此資訊。NAT類型可以是:- Invalid、- UdpBlocked、- OpenInternet、- FullCone或- Symmetric。
- NetworkRunner.IsSharedModeMasterClient:布林值旗標,描述本機同儕節點是否也是- Shared Game Session的- Master Client。這僅在- SharedMode中運行時有效,可用於根據其他客戶端和- Master Client之間的差異來確定應在哪個同儕節點執行哪些特定操作。
另一個相關欄位是NetworkRunner.LobbyInfo,它公開了同儕節點連線到的當前Lobby的資訊(請記住,同儕節點只能處於Lobby、Game Session或中斷連線)。這裡我們列出了LobbyInfo的主要屬性:
- IsValid [bool{get}]:如果- LobbyInfo已準備好進行讀取/寫入,則發出訊號。
- Name [string{get}]:包含當前大廳的名稱。
遊戲階段瀏覽器
遊戲階段瀏覽器在90年代和21世紀初很受歡迎。基本設計仍然有效,但如今他們的主要目的(找到合適的遊戲並快速加入)已被對戰配對所取代。
不鼓勵使用遊戲階段瀏覽器的原因有很多:
- 現時存在安全問題;
- 擷取活躍中遊戲階段清單對於伺服器來說是一項效能繁重的操作;
- 清單總是特定於玩家當前連線的區域; 並且,
- 最重要的是 這是一種過時的設計模式,如今有更好的UX選項可供選擇。
Fusion Matchmaking API提供的最新替代方案包括:
- 為了快速填充/加入遊戲階段:加入隨機開放的房間。
- 為了加入特定類型的比賽:加入一個隨機的開放房間,同時使用遊戲階段屬性進行篩選。
- 為了讓特定玩家加入並一起玩:建立邀請碼和/或按照名稱加入遊戲階段。
除非遊戲有大量運行的社群伺服器,而且這些伺服器有自己的自訂/修改模式和地圖,否則幾乎沒有理由要求提供完整的房間清單,並透過遊戲階段瀏覽器向使用者提供這些房間。
API使用示例
加入隨機遊戲階段
為了加入任何遊戲階段,如果玩家只想快速加入遊戲,只需啟動NetworkRunner,讓它找到一個可用的Game Session,而不需要任何額外參數:
C#
public async Task StartPlayer(NetworkRunner runner) {
  var result = await runner.StartGame(new StartGameArgs() {
    GameMode = GameMode.AutoHostOrClient, // or GameMode.Shared
  });
  if (result.Ok) {
    // all good
  } else {
    Debug.LogError($"Failed to Start: {result.ShutdownReason}");
  }
}
這樣,本機同儕節點將啟動並連線到隨機的Game Session,如果找不到,它將建立一個具有隨機Session Name的新遊戲階段(因為它正在使用GameMode.AutoHostOrClient)。如果使用GameMode.Shared啟動NetworkRunner,對於共享模式下的遊戲階段,這也是有效的。
使用自訂屬性啟動新的遊戲階段
在這個例子中,Host將建立一個具有一些自訂屬性的Game Session,因此稍後,Clients可以使用這些屬性篩選Sessions。
C#
// Some predefined types used as values for the Game Session Properties
public enum GameType : int {
  FreeForAll,
  Team,
  Timed
}
public enum GameMap : int {
  Forest,
  City,
  Desert
}
// Utility method to start a Host using a defined GameMap and GameType
public async Task StartHost(NetworkRunner runner, GameMap gameMap, GameType gameType) {
  var customProps = new Dictionary<string, SessionProperty>();
  customProps["map"] = (int)gameMap;
  customProps["type"] = (int)gameType;
  var result = await runner.StartGame(new StartGameArgs() {
    GameMode = GameMode.Host,
    SessionProperties = customProps,
  });
  if (result.Ok) {
    // all good
  } else {
    Debug.LogError($"Failed to Start: {result.ShutdownReason}");
  }
}
示例代碼顯示了對Game Session的Custom Properties值使用Enums,但這只是為值增加意義的一種方式。
調用runner.StartGame作為Host (GameMode = GameMode.Host),已經足以用Random Name(因為未傳遞SessionName引數)啟動一個新的遊戲階段,並透過使用SessionProperties引數,Fusion將這些屬性包含在Session”中。
使用篩選器加入隨機遊戲階段
考慮到上面的示例程式碼,這裡展示了如何啟動一個Client,該客戶端將加入任何GameMap上的任何Game Session,但具有特定的GameType。
啟動程式碼基本相同,只是GameMode現在設定為GameMode.Client,而customProps僅包含具有所需gameType值的type鍵值。
C#
public async Task StartClient(NetworkRunner runner, GameType gameType) {
  var customProps = new Dictionary<string, SessionProperty>() {
    { "type", (int)gameType }
  };
  var result = await runner.StartGame(new StartGameArgs() {
    GameMode = GameMode.Client,
    SessionProperties = customProps,
  });
  if (result.Ok) {
    // all good
  } else {
    Debug.LogError($"Failed to Start: {result.ShutdownReason}");
  }
}
這足以讓你的客戶端加入一個具有特定GameType的隨機Session。
從大廳加入遊戲階段
找到正確的Game Session的另一種方法是提供一個Sessions清單,允許玩家選擇一個加入。
在這種情況下,加入Lobby是正確的做法,儘管我們強烈建議在 實在 沒有必要的情況下避免這種方法。
對於大多數遊戲類型,基於屬性篩選器加入Session是最好的方式,但Fusion也使Session清單變得非常容易。
與使用通常的流程並如上所述啟動Fusion不同,Session清單遵循的流程略有不同:
- 加入大廳:使用Fusion Runner參照,只需調用NetworkRunner.JoinSessionLobby([SessionLobby], [string]),以使同儕節點連線到Photon Cloud並加入特定的Lobby。此方法接收兩個引數:- SessionLobby:可以是以下值之一:- ClientServer加入預設的- ClientServer Lobby;
- Shared加入預設的- Shared Lobby;及,
- Custom,與自訂- LobbyName結合使用。
 
- LobbyName:這應該是之前建立- Game Session時使用的- Custom Lobby Name。
 
C#
// Utility method to Join the ClientServer Lobby
public async Task JoinLobby(NetworkRunner runner) {
  var result = await runner.JoinSessionLobby(SessionLobby.ClientServer);
  if (result.Ok) {
    // all good
  } else {
    Debug.LogError($"Failed to Start: {result.ShutdownReason}");
  }
}
如前所述,Client可以透過這種方式加入ClientServer、Shared或Custom大廳。
舉個例子,下方顯示了伺服器/主機如何在自訂Lobby中建立Session:
C#
public async Task StartHost(NetworkRunner runner) {
  var result = await runner.StartGame(new StartGameArgs() {
    GameMode = GameMode.Host,
    CustomLobbyName = "MyCustomLobby"
  });
  if (result.Ok) {
    // all good
  } else {
    Debug.LogError($"Failed to Start: {result.ShutdownReason}");
  }
}
以及Client如何加入自訂Lobby:
C#
// Utility method to Join a Custom Lobby
public async Task JoinLobby(NetworkRunner runner) {
  var result = await runner.JoinSessionLobby(SessionLobby.Custom, "MyCustomLobby");
  if (result.Ok) {
    // all good
  } else {
    Debug.LogError($"Failed to Start: {result.ShutdownReason}");
  }
}
- 獲取遊戲階段清單:在使用Fusion時,API的主要入口點之一是 - INetworkRunnerCallbacks,這是Fusion用來展示一系列不同事件的特殊介面,包括- Lobby的遊戲階段清單。每次遊戲階段更改時,無論是建立/刪除遊戲階段還是更新遊戲階段屬性,都會叫用- OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList)回調,並從大廳中獲取完整的遊戲階段清單。- SessionInfo與上述類型相同。然後可以顯示、篩選、排序清單等。
- 加入遊戲階段:選擇要加入的 - Session後,可以使用常用的- NetworkRunner.StartGame()啟動Fusion。但在這種情況下,用於啟動客戶端的- SessionName必須是遊戲階段中的名稱。這樣,- Client將加入該特定的- Game Session。- 像往常一樣選擇正確的GameMode,因為同儕節點正在加入Session,它必須是GameMode.Client或GameMode.Shared模式。
- SessionName欄位必須設定為- SessionInfo.Name,因為這是- Game Session的識別字。
- 所有其他參數都是可選的,應相應地初始化。
 
- 像往常一樣選擇正確的
C#
public class MyBehaviour : Fusion.Behaviour, INetworkRunnerCallbacks {
  // other callbacks...
  // Receive the List of Sessions from the current Lobby
  public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) {
    Debug.Log($"Session List Updated with {sessionList.Count} session(s)");
    // Example
    // Join first session from the list
    // Check if there are any Sessions to join
    if (sessionList.Count > 0) {
      // Get first Session from the list
      var session = sessionList[0];
      Debug.Log($"Joining {session.Name}");
      // Join
      runner.StartGame(new StartGameArgs() {
        GameMode = GameMode.Client, // Client GameMode, could be Shared as well
        SessionName = session.Name, // Session to Join
        // ...
      });
    }
    // OR
    // Example
    // Search the list for a Session with a specific Property
    // Store the target session
    SessionInfo session = null;
    foreach (var sessionItem in sessionList) {
      // Check for a specific Custom Property
      if (sessionItem.Properties.TryGetValue("type", out var propertyType) && propertyType.IsInt) {
        var gameType = (int)propertyType.PropertyValue;
        // Check for the desired Game Type
        if (gameType == 1) {
          // Store the session info
          session = sessionItem;
          break;
        }
      }
    }
    // Check if there is any valid session
    if (session != null) {
      Debug.Log($"Joining {session.Name}");
      // Join
      runner.StartGame(new StartGameArgs() {
        GameMode = GameMode.Client, // Client GameMode, could be Shared as well
        SessionName = session.Name, // Session to Join
        // ...
      });
    }
  }
}