本教程將深入解析 Unity Online Services(UOS)Sync Realtime 的核心功能實現,帶您探索實時多人游戲開發的精髓。
我們將重點介紹:
智能房間管理:輕松實現房間創建、加入與動態管理
實時通信:保障玩家間消息即時傳遞
群組協作:支持群組訂閱、管理及群內溝通
數據持久化:借助記事貼(Sticky Event)保存重要數據
事件緩存:通過緩存事件(Cached Event)同步狀態
服務器調用:支持客戶端與房主的 RPC 式通信
邏輯插件系統:運用 Lua Plugin 實現游戲核心邏輯
通過這些核心功能的實現,開發者可以輕松構建功能完善的實時多人游戲系統。本教程將結合具體代碼示例,詳細講解每個功能的實現方式、使用場景和最佳實踐,幫助您快速掌握 UOS Sync Realtime 的開發技巧,打造流暢的多人游戲體驗!
我們準備了一個基于 Sync Realtime 開發的支持多人同房間信息同步的示例程序,大家可以進入下方的 UOS 官網鏈接,進行在線體驗哦!
https://uos.unity.cn/playground/sync
本教程中涉及 UOS 服務包括:
游戲數據同步服務 Sync Realtime:支持多人同房間信息同步
游戲數據同步服務 Sync Realtime
Sync 服務是由 Unity 資深團隊打造的數據同步服務,為您提供一站式、高并發、低延遲、可靠的游戲同步解決方案!
大家可以通過《》指南,來了解 Sync Realtime 適用的游戲類型:
https://uos.unity.cn/product/multiplayer
Sync Realtime 適用于回合制、社交互動、休閑、體育/賽車、派對類型的游戲。
為充分利用計算資源,Sync 服務會在一個 Pod 內創建多個房間,您可以查看 Pod 的性能指標,如 CPU、內存和網絡流量。同時 Sync 服務支持用戶開啟自助性能測試,可根據應用實際使用情況來測試不同房間配置的服務性能。
下圖展示了 Sync 服務的性能報告的測試詳情、往返時延:
對于 《》指南中的 Sync Relay 服務,已經給大家分享過教程《》,大家也可以點擊下圖進入文章,查看 Sync Relay 結合 Netcode for GameObjects 開發框架,如何來實現高效、穩定的聯機游戲所需的房間管理功能和數據同步機制的。
對于 《》 指南中的 Multiverese 服務,也已經給大家分享過教程 《》 ,大家也可以點擊下圖進入文章,查看如何構建專用服務器(Dedicated Server),并借助 Multiverse 進行托管的。
教程視頻
教程學習大綱
Unity 項目工程初始化準備工作
玩家創建/加入/離開房間
使用事件(一般事件/緩存事件/記事貼)
群組功能(訂閱群組/發送分組消息/退訂群組)
房主調用請求(ServerCall)
添加邏輯插件(Lua Plugin),并將其關聯至房間配置
教程示例程序
教程內學習用到的項目素材資源,是通過在 UOS Launcher 窗口中直接在線安裝的。大家也可以通過下面提供的鏈接查看 Sync Realtime SDK 的示例程序。
Sync Realtime SDK代碼倉庫的地址:
https://unitychina.coding.net/public/uos/SyncRealtimeSDK/git/files/master/Sample~
教程操作步驟
接下來讓我們來看看 Sync Realtime 在項目中的具體用法吧!
1. Unity項目工程初始化準備工作1.1 創建項目工程
打開 Unity Hub,選擇創建「新項目」,教程這里使用的 Unity 編輯器版本是 2022.3.53f1c1 版本,大家可以自行選擇電腦上已安裝的版本。項目模板可以選擇「3D(Built-in)」,自定義項目名稱和位置??梢怨催x選項「啟用游戲云服務」,勾選后會自動為你的項目工程安裝 UOS Launcher 的。
最后點擊「創建項目」,等待項目的創建和加載。
1.2 綁定 UOS App
點擊 Launcher 面板的「LinkApp」按鈕,在彈出窗口中選擇「By Unity project」,在「Select organization」這里選擇一個自己的項目組織,然后在「Select project 」選項這里,我們選擇「Create a new project 」,自行設置修改項目名字「Project name」,然后點擊「Link App」即可。
1.3 開啟 Sync Realtime 服務
在編輯器內 Unity Online Services 窗口的下拉服務列表中,找到「Sync Realtime」,點擊「Enable」開啟服務,并安裝Sync Realtime SDK。
開啟 Sync Realtime 服務后,網頁端進來看到默認已經創建好了一個房間配置。
房間配置參數的含義:
房間配置的超時時長——指一個房間從創建到關閉的最長存續時間,無論房間內是否有玩家,在到期時房間都會自動關閉,默認創建的是 60 分鐘。
空房間的超時時長——指一個房間在沒有玩家的狀態下最長存續的時間,默認顯示是300 秒。房間在超過設定的時間內沒有玩家進入,房間會自動關閉。設置為 0 時,表示不檢查空房間超時。
房間規模——用戶可根據游戲房間的規模、存在時長、負載與性能的需求,自行選擇微型、小型還是中型的房間規模。規模分為微型、小型、中型規模,規模越大表示房間性能越強,默認顯示是微型規模。
在 UOS Launcher 中點擊「import sample」按鈕,即可導入項目示例工程資源包。
該項目是基于 Sync Realtime 開發的一個支持多人同房間信息同步的示例程序, 展示了 SDK 基本接口的使用方式。
導入資源包后,在如圖所示的路徑下,找到 Scene.unity 示例場景并打開。
在彈出的窗口中,點擊「Import TMP Essentials」來導入 TextMeshPro 的字體資源。
游戲的 Game 窗口打開界面如下:
1.5 設置房間配置 ID 號
從 UOS 網頁端 Sync Realtime 的「配置」一欄,找到默認創建的「房間配置 ID」號并復制。
找到場景中 MuninnNetworkManager 對象上的 EventHandler.cs 組件,將復制的「房間配置 ID」號粘貼到 Room Profile UUID 變量上。
1.6 運行游戲測試
我們可以選擇一個在編輯器中 Play 模式下測試,另一個提前 Build 成 exe 文件運行游戲。
構建 Build 文件
將當前場景添加到File → BuildSettings列表中,即可點擊「Build」按鈕,生成一個可執行文件。
如果有需要,可以在Edit → Project Settings頁面,點擊Player按鈕,自定義設置修改要導出的 exe 文件的窗口的大小。例如:這里選擇窗口模式「Windowed」,分辨率設置為 1920 * 1080 像素大小。
同時運行兩個客戶端 exe 文件,游戲界面如下:
2. 創建/加入/離開房間接下來講解下房間創建和加入,以及房間中的各種事件調用是如何實現的!2.1 創建并加入房間
當一個客戶端選擇「創建房間」后,可以先使用默認的房間名稱、命名空間、最大玩家數,然后點擊「創建并加入」按鈕。
UI 界面上按鈕調用相關的事件,都封裝在腳本 UIController.cs 中。
場景中的「創建并加入」按鈕對象上,已經注冊綁定了 UIController.cs 腳本中的 CreateAndJoin 方法。
在 CreateAndJoin 方法中會調用 MuninnNetwork.CreateAndJoinRoom 方法,來實現創建新房間后并進入房間,而 MuninnNetwork 類是 Sync 為客戶端提供的方法調用的接口。
該方法需要傳入一個 CreateRoomRequest 類型的參數,它是一個創建房間的請求對象,包含了創建房間所需的所有配置信息:房間的名稱、房間的命名空間、房間允許的最大玩家數量、房間的可見性(這里設置為公開),以及自定義屬性字典,可以用來存儲額外的房間信息。
///
/// 創建并加入房間
///
public void CreateAndJoin()
{
CreateRoomRequest createRoomRequest = new CreateRoomRequest()
{
Name = nameInput.GetComponent
().text,
Namespace = namespaceInput.GetComponent
().text,
MaxPlayers = Int32.Parse(maxPlayersInput.GetComponent
().text),
Visibility = MuninnRoomVisibility.Public,
CustomProperties = new Dictionary() {
{"k1", "v1"},
{"k2", "v2"},
},
};
MuninnNetwork.CreateAndJoinRoom(createRoomRequest);
}
當玩家成功加入房間后,我們需要處理一些回調事件。而 MuninnBehaviour 類是 Sync Realtime 回調客戶端的接口,開發者需要根據自己游戲邏輯的需要去實現這些接口。
在當前項目中,定義了一個 EventHandler 類,繼承了 MuninnBehaviour 類,實現了相關回調方法。
public class MuninnBehaviour : MonoBehaviour
{
// 成功加入房間
public virtual void OnJoinedRoom(MuninnRoomView roomView){ }
// 收到消息
public virtual void OnEvent(MuninnEvent muninnEvent){ }
// 此處省略其它代碼行......
}
public class EventHandler : MuninnBehaviour
{
// 成功加入房間
public override void OnJoinedRoom(MuninnRoomView roomView){ }
// 收到消息
public override void OnEvent(MuninnEvent muninnEvent)
// 此處省略其它代碼行......
}
當前項目中,EventHandler.cs 腳本已經掛載給了場景中的 MuninnNetworkManager 游戲對象。
在 EventHandler.cs 腳本的 Start 方法中,已經初始化完成了相關的配置信息,比如配置傳輸協議類型、配置 UOS 應用信息、設置房間配置文件的 UUID、配置玩家信息等。
如果是 WebGL 平臺,做出了一些特殊的處理,從 Web 獲取 App 信息、網絡協議、客戶端 ID 等信息。如果房間的 UUID 值為空的話,會有 UI 提示信息告知用戶先輸入 RoomProfileUUID 后再繼續。
所以在項目運行前,才需要大家把房間的 UUID 號,填寫在 Inspector 面板的 EventHandler.cs 組件上。
public class EventHandler : MuninnBehaviour
{
// 此處省略其它代碼行......
private void Start()
{
// 可以選擇不同的協議
MuninnSettings.TransportType = TransportType;
//在加入房間前執行,配置 UOS App 信息
MuninnSettings.UosAppId = Settings.AppID;
MuninnSettings.UosAppSecret = Settings.AppSecret;
MuninnSettings.RoomProfileUUID = RoomProfileUUID;
// 當前玩家用戶信息的配置
MuninnNetwork.PlayerInfo = new MuninnPlayerInfo()
{
Id = userID,//玩家的唯一ID
Name = playerName,
Properties = new Dictionary
{
["key1"] = "val1",
["鍵"] = "值1",
},
};
#if UNITY_WEBGL && !UNITY_EDITOR
Debug.Log("in webgl");
setupMuninnSettings();
#else
if (string.IsNullOrEmpty(RoomProfileUUID))
{
UIController.GetComponent ().ShowErrorMsg( "請在 MuninnNetworkManager 中輸入 RoomProfileUUID 后繼續");
}
#endif
}
}
接著來看代碼,當玩家成功加入房間后,會自動觸發回調函數OnJoinedRoom,來初始化房間狀態和玩家信息。它確保了新加入的玩家能夠快速了解房間狀態,并正確顯示在 UI 界面上。
OnJoinedRoom 方法中接收一個 MuninnRoomView 類型的參數,包含了房間的詳細信息。方法內會做出如下處理:
在 UI 界面上將顯示歡迎消息,消息中會包含房間 ID 號;
由于新的玩家加入房間時,也會回調這個 OnJoinedRoom 方法。所以方法中會遍歷之前玩家在房間中發生的所有緩存事件,然后按照時間順序將全部緩存事件全部發送給新加入的玩家。后續會詳細講解什么是「緩存事件」的。
同時初始化房間信息,設置房主 ID、當前玩家的 SenderId,并更新 UI 界面上的玩家列表顯示;
最后有一個特殊平臺的處理:當游戲在 WebGL 平臺運行時,會將房間 ID 傳遞給 WebGL 平臺的,來實現 Unity 游戲和 WebGL 平臺之間的通信。
[DllImport("__Internal")]
private static extern void SetRoomIdToWeb(string roomId);
///
/// 加入房間成功
///
///
public override void OnJoinedRoom(MuninnRoomView muninnRoomView)
{
UIController.GetComponent
().AddMessageItem(
$"歡迎加入房間
{muninnRoomView.Room.Id}
。",
0, MessageType.systemMessage);
foreach (MuninnCachedEvent e in muninnRoomView.CachedEvents)
{
// 每條 cached 單獨展示
UIController.GetComponent
().AddMessageItem(Encoding.UTF8.GetString(e.Data,
0, e.Data.Length), e.SenderId, MessageType.otherMessage);
}
// 初始化房主 id 和 玩家 id
masterClientId = muninnRoomView.MasterClientId;
playerSenderId = muninnRoomView.SenderId;
UpdatePlayerList();
UIController.GetComponent
().JoinedRoomHandler();
#if UNITY_WEBGL && !UNITY_EDITOR
SetRoomIdToWeb(muninnRoomView.Room.Id);
#endif
}
此時 Game 窗口的 UI 顯示如下,會看到信息「歡迎進入房間 + 房間 ID 號」。
2.2 加入房間
另一個客戶端選擇「加入房間」時,需要先手動輸入一下「房間 UUID」??梢栽?strong>「Sync Realtime」→「房間管理」頁面,選擇當前創建的房間,復制一下「房間 UUID」。
在游戲窗口中輸入「房間 UUID」后,點擊「加入房間」按鈕。
「加入房間」的 Button 上注冊綁定了 JoinRoomById 方法,在該方法內會通過調用MuninnNetwork.JoinRoomByUUID 方法,允許玩家通過輸入房間 ID 號直接加入特定房間。
///
/// 通過房間 ID 加入房間
///
public void JoinRoomById()
{
string id = RoomId.GetComponent ().text;
if (id == "")
{
OnError.Invoke("請輸入房間 ID。");
return;
}
MuninnNetwork.JoinRoomByUUID(id);
}
Game 窗口的 UI 界面上能看到提示信息:
當有新玩家加入房間時的回調方法:
新玩家成功加入房間時,新玩家自己同樣的會自動觸發回調方法 OnJoinedRoom;
///
/// 加入房間成功
///
public override void OnJoinedRoom(MuninnRoomView muninnRoomView)
{
UIController.GetComponent().AddMessageItem($"歡迎加入房間 {muninnRoomView.Room.Id}。", 0, MessageType.systemMessage);
foreach (MuninnCachedEvent e in muninnRoomView.CachedEvents)
{
// 每條 cached 單獨展示
UIController.GetComponent().AddMessageItem(Encoding.UTF8.GetString(e.Data, 0, e.Data.Length), e.SenderId, MessageType.otherMessage);
}
// 初始化房主 id 和 玩家 id
masterClientId = muninnRoomView.MasterClientId;
playerSenderId = muninnRoomView.SenderId;
UpdatePlayerList();
UIController.GetComponent().JoinedRoomHandler();
#if UNITY_WEBGL && !UNITY_EDITOR
SetRoomIdToWeb(muninnRoomView.Room.Id);
#endif
}
當新玩家成功加入房間時,房間內的其他人會收到響應的 OnPlayerEnteredRoom回調方法。
///
/// 玩家進入房間
///
///
public override void OnPlayerEnteredRoom(MuninnPlayer muninnPlayer)
{
// 獲取玩家的頭像、名稱
UIController.GetComponent
().AddMessageItem(
$"
{muninnPlayer.Name}{muninnPlayer.SenderId}
進入房間。",
0, MessageType.systemMessage);
UpdatePlayerList();
}
新玩家加入房間失敗時
如果輸入的房間 UUID 號錯誤,導致無法加入房間時,Game 窗口 UI 界面如下:
加入房間失敗時,會自動觸發回調方法OnJoinRoomFailed的。
///
/// 加入房間失敗
///
///
public override void OnJoinRoomFailed(MuninnError error)
{
Debug.Log("加入房間失敗時的回調");
UIController.GetComponent ().OnError.Invoke(MuninnCodeLocalize.GetCodeName(error.Code));
}
2.3 離開房間
如果有客戶端點擊「退出房間」按鈕,退出房間時,在其它客戶端 UI 界面上可以看到提示的信息:
「退出房間」的按鈕上已經注冊綁定了 LeaveRoom 方法,用于處理玩家離開房間時的邏輯。
分析代碼:
在 LeaveRoom 方法中首先會獲取到當前房間的信息,然后根據房間內玩家數量決定是關閉房間還是僅退出房間;
如果房間內只有當前玩家自己的話,就調用 MuninnNetwork.CloseRoom 方法來關閉整個房間;
如果房間內還有其他玩家時,調用 MuninnNetwork.Disconnect 方法,僅當前玩家退出房間。
///
/// 離開房間
///
public void LeaveRoom()
{
MuninnRoomView view = MuninnNetworkManager.GetComponent ().GetMuninnRoomView();
if (view != null && view.Players.Count <= 1)
{
// 關閉房間
MuninnNetwork.CloseRoom(view.Room.Id, CloseRoomCallback);
Debug.Log("CloseRoom");
}
else
{
// 主動關閉連接
MuninnNetwork.Disconnect();
Debug.Log("Disconnect");
}
}
///
/// 離開房間執行的回調
///
///
public void CloseRoomCallback(CloseRoomResponse code)
{
Debug.Log("CloseRoomCallback");
}
當房間內只要有玩家離開時,都會自動觸發回調方法 OnDisconnected,來處理斷開連接時能夠正確清理游戲狀態和更新 UI 界面。
///
/// 斷開連接
///
public override void OnDisconnected()
{
Debug.Log("主動斷開連接時的回調");
UIController.GetComponent ().LeaveRoomHandler();
}
當有玩家離開房間時,房間內的其他玩家會收到響應的 OnPlayerLeftRoom 回調方法,方法內參數 muninnPlayer 表示離開的玩家信息。這樣能確保當有玩家離開時,其他玩家能夠及時收到通知,并且房間狀態能夠正確更新。這對于維護游戲體驗和房間狀態的一致性非常重要。
///
/// 玩家離開房間
///
///
public override void OnPlayerLeftRoom(MuninnPlayer muninnPlayer)
{
Debug.Log("玩家離開房間時的回調");
UIController.GetComponent ().AddMessageItem(String.Format( "{0}{1} 離開房間。", muninnPlayer.Name, muninnPlayer.SenderId), 0, MessageType.systemMessage);
UpdatePlayerList();
}
2.4 房主變更
當原房主離開房間、斷開鏈接或者被踢出房間等可能的情況時,房間的房主(MasterClient)發生變更,會觸發回調方法 OnMasterClientChanged,方法會接收新房主的 ID(masterClientId)。方法內會更新當前房間的房主 ID,并更新房間內的玩家列表。
///
/// master client 變更
///
///
public override void OnMasterClientChanged(uint masterClientId)
{
Debug.Log("master client 變更時的回調");
base.OnMasterClientChanged(masterClientId);
this.masterClientId = masterClientId;
this.UpdatePlayerList();
}
3. 事件(一般事件/緩存事件/記事貼
事件是 Sync 同步信息的載體,分為一般事件(Event)、緩存事件(Cached Event)和記事貼(Sticky Event)。
3.1 一般事件
一般事件用于玩家之間的信息同步與共享,它是實時發送和接收的,不會被房間存儲。發送者可以選擇接收方范圍,只有在接收范圍內的玩家才會收到事件。
Game 窗口界面如下:
「發送一般事件」的按鈕上已經注冊綁定了方法 SendMessage,在方法內通過調用 MuninnNetwork.RaiseEvent 來發送一般事件到房間,實現玩家之間的實時消息傳遞。
該方法需要傳入 2 個參數:發送的消息內容、事件的發送選項,在這里使用的是 RaiseEventTarget.TO_ALL,表示發送給房間內所有玩家。
///
/// 發送一般事件
///
public void SendMessage()
{
string msg = "接收到一般事件。事件是 Sync 同步信息的載體,分為一般事件(Event)、緩存事件(Cached Event)和記事貼(StickyEvent)。當前為一般事件,用于玩家之間的信息同步與共享,它是實時發送和接收的,不會被房間存儲。發送者可以選擇接收方范圍,只有在接收范圍內的玩家才會收到事件。";
MuninnNetwork.RaiseEvent(Encoding.UTF8.GetBytes(msg), new RaiseEventOptions() { Target = RaiseEventTarget.TO_ALL });
}
給大家詳細解釋下參數RaiseEventTarget的使用情形:
public enum RaiseEventTarget
{
TO_ALL,
TO_GROUPS,
TO_ALL_BUT_ME,
TO_PLAYERS,
TO_MASTER,
TO_PLUGIN,
}
RaiseEventTarget主要來控制事件的發送范圍,我們可以設置不同的接收目標來實現靈活的消息傳遞機制:
TO_ALL:發送給房間內的所有玩家(包括發送者自己),是最常用的廣播方式,適合需要全員通知的場景;
TO_GROUPS:發送給特定群組的玩家,需要指定目標群組(搭配 TargetGroups 參數使用),用于分組通信;
TO_ALL_BUT_ME:發送給房間內除了自己以外的所有玩家,適合不需要自己接收的消息;
TO_PLAYERS:發送給指定的玩家列表,需要指定具體的玩家 ID(搭配 ReceiverIds 參數使用),用于點對點通信,適合私聊或特定玩家通知;
TO_MASTER:發送給房主(Master Client),用于與房主通信,適合需要房主處理的消息;
TO_PLUGIN:發送給插件處理,用于與服務器插件通信,適合需要服務器處理的邏輯。
當收到一般事件、分組消息、或者收到其他玩家發送的消息時,都會自動觸發回調方法 OnEvent,通過消息類型的區分,可以在 UI 上以不同的方式顯示不同類型的消息。
方法內會判斷消息類型:如果發送者 ID 為0,是插件發送的消息;如果發送者是自己,是自己發送的消息,否則是其他玩家發送的消息。
///
/// 接收到一般事件、分組消息
///
///
public override void OnEvent(MuninnEvent e)
{
Debug.Log("接收到一般事件、分組消息時的回調:" + Encoding.UTF8.GetString(e.Data));
var type = e.SenderId == 0 ? MessageType.pluginMessage : (playerSenderId == e.SenderId ? MessageType.myMessage : MessageType.otherMessage);
UIController.GetComponent ().AddMessageItem(Encoding.UTF8.GetString(e.Data), e.SenderId, type);
}
3.2 緩存事件(Cached Event)
緩存事件用于記錄里程碑事件,只有在每個新玩家加入房間時,房間才會把之前所有的緩存事件按照時間順序全部發送給他們。
Game 窗口界面如下:
3.2.1添加緩存事件
按鈕上已經注冊綁定了一個用于在房間中添加緩存事件的函數 AddCached ,主要用于添加緩存事件的功能。
緩存事件是一種特殊的事件類型,適合用于記錄房間中的重要里程碑事件,這樣可以確保新加入的玩家能夠了解房間中發生過的歷史事件,保持游戲狀態的一致性。
分析代碼:
調用 MuninnNetwork.AddCachedEvent 方法來添加緩存事件,會需要傳入 3 個參數:
使用固定的 key 作為事件的標識符;
在變量 msg 中解釋了緩存事件的作用,并將消息內容轉換為 UTF8 編碼的字節數組;
通過回調函數 response 來處理添加的結果:如果添加成功,將顯示成功消息、提示用戶可以在其他客戶端測試以及顯示移除緩存事件的按鈕。如果失敗,則顯示出錯誤信息。
///
/// 添加緩存事件
///
public void AddCached()
{
String key = "key1";
string msg = "接收到緩存事件。緩存事件用于記錄里程碑事件,只有在每個玩家加入房間時,房間才會把之前所有的緩存事件按照時間順序全部發送給他們。";
MuninnNetwork.AddCachedEvent(key, Encoding.UTF8.GetBytes(msg), ((response) =>
{
if (response.Code == (uint)MuninnCode.OK)
{
AddMessageItem("添加緩存事件成功。", 0, MessageType.systemMessage);
AddMessageItem("Tips: 在另一客戶端點擊「退出房間」再重新「加入房間」", 0, MessageType.systemMessage);
ShowRemoveCachedButton();
}
else
{
AddMessageItem(MuninnCodeLocalize.GetCodeName(response.Code), 0, MessageType.systemMessage);
}
}));
}
3.2.2移除緩存事件
當某個緩存事件不再需要時,可以移除該緩存事件。移除緩存事件與添加緩存事件功能配合使用,形成完整的事件管理機制。
可以點擊 Game 窗口中的 UI 按鈕來「移除緩存事件」,Game 窗口界面如下:
按鈕上注冊綁定了 RemoveCached 方法,在方法內會調用 MuninnNetwork.RemoveCachedEvent 來移除房間中已經添加過的緩存事件。
RemoveCachedEvent 方法需要傳入兩個參數:
要使用與添加緩存事件時相同的 key,這樣可以確保刪除的是之前添加的那個特定事件;
通過回調函數 response 來處理刪除結果:如果刪除成功,在界面上顯示刪除成功的消息并顯示添加緩存事件的按鈕。
///
/// 移除緩存事件
///
public void RemoveCached()
{
String key = "key1";
MuninnNetwork.RemoveCachedEvent(key, ((response) =>
{
if (response.Code == (uint)MuninnCode.OK)
{
AddMessageItem("移除緩存事件成功。", 0, MessageType.systemMessage);
ShowAddCachedButton();
}
else
{
AddMessageItem(MuninnCodeLocalize.GetCodeName(response.Code), 0, MessageType.systemMessage);
}
}));
}
3.3 記事貼(Sticky Event)
記事貼用于玩家在房間服務內保存數據,數據以鍵值對(key-value)的形式保存。記事貼的范圍(scope)分為 Room 和 Player。前者允許所有玩家讀寫,后者只允許創建者玩家讀寫。
現在先以記事貼的范圍為 Room 來講解,Game 窗口畫面為:
3.3.1添加記事貼
「添加記事貼」按鈕上注冊綁定了方法 SetSticky,方法內會調用 MuninnNetwork.SetStickyEvent來添加一個記事貼,它提供了數據持久化的機制,允許在房間服務中保存和共享數據,支持鍵值對形式的數據存儲。
通過記事貼,可以實現游戲狀態的保存和恢復,以及玩家之間的數據共享。
SetStickyEvent 方法內需要傳入 4 個參數:
記事貼的唯一標識符 "sticky_key1",用于后續獲取或更新記事貼;
記事貼的具體內容數據:data;
記事貼的作用范圍,當前設置的是房間級別的(Room),即房間內的所有玩家可以讀寫;
回調函數 response 來處理設置記事貼的結果。
///
/// 添加記事貼
///
public void SetSticky()
{
EventHandler handler = MuninnNetworkManager.GetComponent
();
string data = $"接收到{handler.publicPlayerName}發送的 key 為“sticky_key1”的記事貼。記事貼用于玩家在房間服務內保存數據,數據以鍵值對(key-value)的形式保存。記事貼的范圍(scope)分為 Room 和 Player。前者允許所有玩家讀寫,后者只允許創建者玩家讀寫。";
byte[] bytes = Encoding.UTF8.GetBytes(data);
MuninnNetwork.SetStickyEvent("sticky_key1", bytes, StickyEventScope.Player, (response =>
{
if (response.Code == (uint)MuninnCode.OK)
{
// 成功
AddMessageItem("添加key 為“sticky_key1”的記事貼成功。", 0, MessageType.systemMessage);
AddMessageItem("Tips: 在任一客戶端點擊「獲取記事貼」試試吧!", 0, MessageType.systemMessage);
ShowRemoveStickyButton();
}
else
{
AddMessageItem("添加key 為“sticky_key1”的記事貼失敗。", 0, MessageType.systemMessage);
AddMessageItem(MuninnCodeLocalize.GetCodeName(response.Code), 0, MessageType.systemMessage);
}
}
));
}
3.3.2移除記事貼
在 RemoveSticky 方法內,會調用 MuninnNetwork.RemoveStickyEvent 方法來刪除房間中的記事貼,用以清理房間中的持久化數據。
RemoveStickyEvent 方法需要傳入 3 個參數:指定要刪除的記事貼的 key、記事貼的作用范圍、以及處理移除操作結果的回調函數 response 。
///
/// 移除記事貼
///
public void RemoveSticky()
{
MuninnNetwork.RemoveStickyEvent("sticky_key1", StickyEventScope.Room, (response =>
{
if (response.Code == (uint)MuninnCode.OK)
{
AddMessageItem("移除key 為“sticky_key1”的記事貼成功。", 0, MessageType.systemMessage);
ShowSetStickyButton();
}
else
{
AddMessageItem("移除key 為“sticky_key1”的記事貼失敗。", 0, MessageType.systemMessage);
AddMessageItem(MuninnCodeLocalize.GetCodeName(response.Code), 0, MessageType.systemMessage);
}
}
));
}
3.3.3獲取記事貼
在方法 GetSticky 內通過調用 MuninnNetwork.GetStickyEvent,來獲取指定鍵值的記事貼內容,從房間服務中讀取持久化數據。
GetStickyEvent 方法需要傳入 3 個參數:要獲取的記事貼的 key、記事貼的作用范圍,以及處理獲取結果的回調函數 response,以確保數據的正確獲取和顯示。
注意事項:如果發布記事貼的人離開了房間,就獲取不到記事貼了。
// 獲取記事貼
public void GetSticky()
{
MuninnNetwork.GetStickyEvent("sticky_key1", StickyEventScope.Room, HandleStickyResp);
}
// 處理 sticky 的回調
public void HandleStickyResp(MuninnGetStickyEventResponse resp)
{
if (resp == null) return;
StickyEvent e = resp.StickyEvent;
if (e == null) return;
AddMessageItem(Encoding.UTF8.GetString(e.Data, 0, e.Data.Length), e.SenderId, MessageType.otherMessage);
}
3.3.4修改記事貼的范圍:Room → Player
記事貼的范圍(StickyEventScope) 分為 Room 和 Player,二者的區別是:Room 會允許所有玩家讀寫記事貼的內容,而 Player 是只允許創建者玩家讀寫。
我們來測試下效果,腳本中需要修改代碼的地方為:
在設置記事貼的方法中修改范圍為 Player:
///
/// 添加記事貼
///
public void SetSticky()
{
EventHandler handler = MuninnNetworkManager.GetComponent ();
string data = $"接收到{handler.publicPlayerName}發送的 key 為“sticky_key1”的記事貼。記事貼用于玩家在房間服務內保存數據,數據以鍵值對(key-value)的形式保存。記事貼的范圍(scope)分為 Room 和 Player。前者允許所有玩家讀寫,后者只允許創建者玩家讀寫。";
byte[] bytes = Encoding.UTF8.GetBytes(data);
MuninnNetwork.SetStickyEvent("sticky_key1", bytes, StickyEventScope.Player, (response =>
{
//此處省略其它代碼行......
}
));
}
在獲取記事貼的方法中修改范圍為 Player:
///
/// 獲取記事貼
///
public void GetSticky()
{
MuninnNetwork.GetStickyEvent("sticky_key1", StickyEventScope.Player, HandleStickyResp);
}
然后,游戲運行后大家可以再次測試下,只有創建記事貼的玩家才能讀寫記事貼的內容了。
4. 群組(訂閱群組/發送分組消息/退訂群組)
群組是同一房間內玩家的子集。群組內信息共享,組外無法讀寫組內的信息。根據您的游戲邏輯的需要,玩家可以訂閱或退訂群組。群組內的玩家在離開房間重新登錄后,需要重新訂閱群組。
游戲運行后,Game 窗口如下:
4.1 訂閱興趣組
「加入分組」的按鈕上注冊綁定了方法 SubScribeGroup,在方法內調用 MuninnNetwork.SubscribeGroups 來使當前玩家加入某些特定群組,支持多個群組的訂閱,然后接收該群組的消息。通過群組機制,可以實現團隊協作、分組游戲等功能。
在 SubscribeGroups 方法內可以傳入要訂閱的群組 ID 列表(groups),興趣組范圍是 [1, 255] 之間, 0 是系統默認保留(不需要訂閱)。
///
/// 訂閱分組
///
public void SubScribeGroup()
{
List groups = new List();
groups.Add(1);
MuninnNetwork.SubscribeGroups(groups);
}
當成功訂閱群組后,自動觸發回調方法 OnSubscribeGroups 來處理群組訂閱成功的結果。
///
/// 訂閱分組成功
///
///
public override void OnSubscribeGroups(MuninnSubscribeGroupResponse rsp)
{
base.OnSubscribeGroups(rsp);
UIController.GetComponent
().AddMessageItem("加入分組成功。", 0, MessageType.systemMessage); UIController.GetComponent
().AddMessageItem("Tips: 點擊「發送分組消息」跟群組內玩家通信吧!", 0, MessageType.systemMessage); UIController.GetComponent
().ShowUnsubGroupButton(); }
當訂閱群組失敗時,自動觸發回調方法 OnSubscribeGroupsFailed,這個回調方法是群組管理系統中重要的錯誤處理機制,它確保了在群組訂閱失敗時能夠及時通知用戶,并提供適當的錯誤信息,幫助用戶理解和解決問題。
///
/// 訂閱分組失敗
///
///
public override void OnSubscribeGroupsFailed(MuninnError error)
{
Debug.LogWarningFormat("[Muninn]: OnSubscribeGroupsFailed() was called by Muninn");
UIController.GetComponent ().AddMessageItem(MuninnCodeLocalize.GetCodeName(error.Code), 0, MessageType.systemMessage);
}
4.2 發送分組消息
發送分組消息,可以實現向特定群組發送消息的功能,實現了群組內的定向通信,適合團隊協作和分組游戲的場景。它和發送一般事件一樣,調用的也是 MuninnNetwork.RaiseEvent 方法。
在發送選項(RaiseEventOptions)中需要指明發送目標群組的 ID,支持多個群組同時發送,群組外的玩家無法接收消息。
///
/// 發送分組消息
///
public void SendGroupMessage()
{
// 這是一條組播消息
string msg = "接收到群組消息。群組是同一房間內玩家的子集。群組內信息共享,組外無法讀寫組內的信息。根據您的游戲邏輯的需要,玩家可以訂閱或退訂群組。群組內的玩家在離開房間重新登陸后,需要重新訂閱群組。";
uint[] grps = new uint[] { 1 };
MuninnNetwork.RaiseEvent(Encoding.UTF8.GetBytes(msg), new RaiseEventOptions() { Target = RaiseEventTarget.TO_GROUPS, TargetGroups = grps });
}
當收到分組消息時,同樣的也是會自動觸發回調方法 OnEvent。
///
/// 接收到一般事件、分組消息
///
///
public override void OnEvent(MuninnEvent e)
{
Debug.Log("接收到一般事件、分組消息時的回調:" + Encoding.UTF8.GetString(e.Data));
var type = e.SenderId == 0 ? MessageType.pluginMessage : (playerSenderId == e.SenderId ? MessageType.myMessage : MessageType.otherMessage);
UIController.GetComponent ().AddMessageItem(Encoding.UTF8.GetString(e.Data), e.SenderId, type);
}
4.3 退訂興趣組
調用 MuninnNetwork.UnsubscribeGroups方法時,允許玩家退出群組,停止接收群組消息,為群組管理提供了完整的生命周期控制。通過這個方法,玩家可以靈活地管理自己的群組訂閱狀態。
在方法 UnsubscribeGroups 中需要傳入要退出的群組 ID 列表(groups),支持多個群組同時取消訂閱。
///
/// 取消訂閱分組
///
public void UnsubscribeGroup()
{
List groups = new List();
groups.Add(1);
MuninnNetwork.UnsubscribeGroups(groups);
Debug.Log("subscribe group 1");
}
當用戶成功取消訂閱群組后,會自動觸發回調方法 OnUnsubscribeGroups,來處理群組取消訂閱成功的結果。
///
/// 取消訂閱分組成功
///
///
public override void OnUnsubscribeGroups(MuninnUnsubscribeGroupsResponse rsp)
{
Debug.Log("取消訂閱分組成功時的回調");
base.OnUnsubscribeGroups(rsp);
UIController.GetComponent ().AddMessageItem( "退出分組成功。", 0, MessageType.systemMessage);
UIController.GetComponent ().ShowSubscribeGroupButton();
}
當群組 ID 無效或者群組不存在等情況,導致用戶取消訂閱群組操作失敗時,會自動觸發回調方法 OnUnsubscribeGroupsFailed。能確保在群組取消訂閱失敗時能夠及時通知用戶,并提供適當的錯誤信息,幫助用戶理解和解決問題。通過日志記錄和 UI 提示,提供了完整的錯誤反饋機制。
///
/// 取消訂閱分組失敗
///
///
public override void OnUnsubscribeGroupsFailed(MuninnError error)
{
Debug.Log("取消訂閱分組失敗時的回調");
Debug.LogWarningFormat("[Muninn]: OnUnsubscribeGroupsFailed() was called by Muninn");
UIController.GetComponent ().AddMessageItem(MuninnCodeLocalize.GetCodeName(error.Code), 0, MessageType.systemMessage);
}
5. 房主調用請求(ServerCall)
在實時模式下,玩家客戶端可以選擇直接調用房主端上的服務端邏輯(類似 RPC)。屆時,房主端會收到 OnServerCall 事件。
「server call」按鈕上注冊綁定了方法 ServerCall,然后調用 MuninnNetwork.ServerCall 方法,實現客戶端與房主通信,允許客戶端向房主發送服務器調用請求,實現類似 RPC(遠程過程調用)的功能,方法內傳入準備要發送的數據(data)即可。
///
/// 發起 serve call
///
public void ServerCall()
{
string data = "接收到 server call。在實時模式下,玩家客戶端可以選擇直接調用房主端上的方法(類似 RPC)。屆時,房主端會收到 OnServerCall 事件。";
byte[] bytes = Encoding.UTF8.GetBytes(data);
MuninnNetwork.ServerCall(bytes);
}
當房主收到服務器調用請求時,會自動觸發回調方法 OnServerCall,用于處理客戶端發來的請求。
///
/// 接收到 server call
///
///
public override void OnServerCall(MuninnEvent e)
{
Debug.Log("接收到 server call 時的回調");
UIController.GetComponent ().AddMessageItem(Encoding.UTF8.GetString(e.Data), e.SenderId, playerSenderId == e.SenderId ? MessageType.myMessage : MessageType.otherMessage);
}
6. 添加邏輯插件(Lua Plugin),并將其關聯至房間配置6.1 邏輯插件(Plugin)介紹
邏輯插件(Plugin)是專為 Sync Realtime 開發者提供的自定義插件功能。您可以使用 Lua、JavaScript 等腳本語言編寫插件,通過重載內置的事件處理方法,實現服務端的自定義處理邏輯。支持例如發送消息、定時任務、Webhook等應用場景, 其工作流程如下:
https://uos.unity.cn/doc/sync/realtime-plugin#overview
圖中綠色為 Lua 腳本可以 hook 的方法,藍色為 Sync Realtime 處理邏輯。
6.2 創建 Lua 邏輯插件
在 UOS 網頁端我們為大家提供了一份 Lua 語言的邏輯插件的代碼示例程序,大家可以直接下載使用。
下載完示例程序后,可以解壓縮文件,自行打開查看下 main.lua 腳本文件中代碼,如下所示:
local muninn = require("MuninnPlugin")
local MessageType = muninn.MessageType
local ReturnType = muninn.ReturnType
-- [生命周期] 創建房間成功后
function OnCreateRoom(room)
muninn.LogInfo("[plugin] OnCreateRoom")
-- 給當前所有玩家定時發送消息
muninn.ScheduleRepeat("SendMessage", 10 * 1000)
end
-- 定時任務函數
function SendMessage()
players, err = muninn.GetOnlinePlayers()
for key, player in pairs(players) do
local currentDateTime = os.date("%Y-%m-%d %H:%M:%S")
muninn.SendMessage(player.Id, "[plugin] 每 10s 執行定時任務發送消息,當前接收者為玩家" .. player.Id .. "。" .. currentDateTime)
muninn.LogInfo("[plugin] 每 10s 執行定時任務發送消息,當前接收者為玩家" .. player.Id .. "。" .. currentDateTime)
end
end
-- [生命周期] 關閉房間前
function BeforeCloseRoom(room)
muninn.LogInfo("[plugin] BeforeCloseRoom")
end
-- [生命周期] 關閉房間后
function OnClose(room)
muninn.LogInfo("[plugin] OnClose")
end
-- [生命周期] 玩家加入房間前
function BeforeJoin(player)
muninn.LogInfo("[plugin] BeforeJoin")
return muninn.ReturnType.CONTINUE
end
-- [生命周期] 玩家加入房間后
function OnJoin(player)
muninn.LogInfo("[plugin] OnJoin")
muninn.SendMessage(player.Id, "[plugin] " .. player.Name .. " 歡迎加入房間!")
local currentDateTime = os.date("%Y-%m-%d %H:%M:%S")
muninn.SendMessage(player.Id, "[plugin] 當前服務器時間為" .. currentDateTime)
end
-- [生命周期] 玩家離開房間
function OnLeave(player)
muninn.LogInfo("[plugin] OnLeave")
end
-- [生命周期] 玩家發起的事件
function OnMessage(msg)
muninn.LogInfo("[plugin] OnMessage => " .. msg.Data .. " " .. tostring(msg.Target) )
muninn.SendMessage(msg.SenderId, "[plugin] 接收到來自玩家" .. msg.SenderId .. "的消息")
return muninn.ReturnType.CONTINUE
end
大家可以進入 UOS 網頁端,查看「Lua Plugin 集成指南」,來了解 Lua 插件生命周期的各個方法以及提供的功能函數簇的用法。
https://uos.unity.cn/doc/sync/realtime-plugin-lua#onCreateRoom
回到 UOS 網頁端,找到「Sync Realtime → 邏輯插件」,點擊「立即創建」按鈕來創建一個邏輯插件。
在彈出窗口中,填入邏輯插件的名稱、選擇邏輯插件的腳本語言類型(Lua),然后上傳邏輯插件的 zip 格式的壓縮包文件,并點擊「提交」按鈕。
6.3 邏輯插件關聯至房間配置
接著點擊剛才創建的邏輯插件(PluginDemo)的最右側按鈕,選擇「關聯至房間配置」。
在彈窗中選擇一個需要關聯的房間配置,然后點擊「關聯」即可。
點擊插件的名稱,可進入邏輯插件詳情頁,在此頁面可以查看邏輯插件的基本信息、已關聯的房間配置列表(可以在該列表關聯新的房間配置或者取消關聯),以及支持修改邏輯插件名稱和下載上傳的邏輯插件包。
6.4 運行邏輯插件,查看 Plugin 日志
項目運行后,UI 界面上看到的帶有「plugin」前綴的信息,都是關聯的邏輯插件發出來的。
然后在「房間管理」頁面,找到使用了邏輯插件的房間,可點擊查看 Plugin 運行后打印的日志。
邏輯插件詳細日志信息如下:
寄語
我們即將推出基于 UOS Sync Realtime 開發的聯機示例游戲項目 ——《火拼 24 點》教程。在開發過程中,Lua Plugin 邏輯插件發揮著關鍵作用。
當《火拼 24 點》游戲房間內人數湊齊兩人,Lua Plugin 便會迅速篩選出對局題目,并精準下發到客戶端。在緊張刺激的答題環節,為有效杜絕作弊行為,插件會接收玩家提交的數字編號與計算過程,嚴謹驗證答案的正確性。待游戲進入結算階段,插件又會將完整的對局過程匯報給 Func,助力服務器端的排行榜數據的更新 。
更多精彩功能與實用開發技巧,我們會在教程中一一揭秘,敬請關注!
本期教程我們先講解到這里,大家趕快打開項目試試吧!
Unity Online Services (UOS) 是一個專為游戲開發者設計的一站式游戲云服務平臺,提供覆蓋游戲全生命周期的開發、運營和推廣支持。
了解更多 UOS 相關信息:
官網:https://uos.unity.cn
技術交流 QQ 群:823878269
公眾號:UOS 游戲云服務
Unity 官方微信
第一時間了解Unity引擎動向,學習進階開發技能
每一個“點贊”、“在看”,都是我們前進的動力
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.