Func Realtime 接入指南
Func Realtime 接入指南
注意:Func Realtime 默认是关闭的,可以 联系我们 开启。
1.概述
Func Realtime 是基于 Sync Realtime + Func Stateful 开发的一款数据同步服务,为您提供大型多人服务器中游戏数据同步的解决方案,适配例如元宇宙或者需要超长时间房间的应用或游戏。
- 支持主流传输协议,如KCP(可靠UDP),WebSocket,UTP(Unity Transport Protocol)等,且允许不同客户端以不同的传输协议进行同步。
- 每台服务器都是一个游戏房间,允许您充分利用服务器完整资源,实现大型在线互动的长期数据留存。
- 支持服务器弹性伸缩策略,可根据玩家流量实现分服管理。
房间(Room)
房间负责同步服务。在 Func Realtime 中,每个房间都会被分配给不同的服务器,而您无需顾虑连接请求设计,我们的 SDK 会协助您的游戏客户端与房间连接和交互。
玩家(Player)
玩家即是您的游戏玩家,他们通过房间进行信息的同步。
事件(Event)
事件是同步信息的载体,分为一般事件(Event)、缓存事件(Cached Event)和记事贴(Sticky Event)。
- 一般事件用于玩家之间的信息同步与共享,它是实时发送和接收的,不会被房间存储。发送者可以选择接收方范围,只有在接收范围内的玩家才会收到事件。
- 缓存事件用于记录里程碑事件,只有在每个玩家加入房间时,房间才会把之前所有的缓存事件按照时间顺序全部发送给他们。
- 记事贴用于玩家在房间服务内保存数据,数据以键值对(key-value)的形式保存。记事贴的范围(scope)分为 Room 和 Player。前者允许所有玩家读写,后者只允许创建者玩家读写。
群组(Group)
群组是同一房间内玩家的子集。群组内信息共享,组外无法读写组内的信息。根据您的游戏逻辑的需要,玩家可以订阅或退订群组。群组内的玩家在离开房间重新登录后,需要重新订阅群组。
房主
Func Realtime 保留了一般游戏匹配设计中的房主概念,允许您实现如踢出玩家、自定义服务调用等功能。每个时刻下,房主身份将自动保留给当前最先加入房间的玩家,也可被手动切换。而客户端可以选择直接调用房主端上的方法(类似 RPC),届时房主端会收到 OnServerCall 事件。
2.安装配置 UOS Launcher
参考 Launcher 教程,安装 Launcher 后,关联 UOS APP, 开启 Func Stateful 和 Func Realtime 服务并安装 Func Stateful 和 Func Realtime SDK。
注意: 请务必确认在进行后续教程之前,已经成功完成了 Launcher 教程的安装步骤,否则可能导致后续接入无法顺利进行。
3.配置服务端
为正常使用 Func Realtime 服务,您需要在 UOS官网 中对您的项目进行相关配置。
3.1.部署Realtime镜像
通过我们提供的镜像,您可创建服务端程序。在 镜像 选项卡选择中,点击「上传新的服务器程序」,填写标签。选择 URL 模式,填入 https://uos-1314001764.cos.ap-shanghai.myqcloud.com/FuncRealtime/func-realtime-v1.0.zip ,在需要增加权限的文件列表中添加 main ,点击「制作镜像」。
注意:上图红框部分不可更改。
3.2.创建启动配置
等待镜像完成后,点击「将新镜像添加至启动配置」
在镜像启动参数中,填入以下信息,点击「添加镜像配置」:
- 服务器端口:TCP/8081/ws、UDP/8082/utp、UDP/8083/kcp2k、UDP/8085/utp2、TCP/8090/http
- 入口程序启动命令:main

注意:上图红框部分不可更改。
在环境变量中,添加如下内容,点击「更新环境变量」:
- UOS_APP_ID: 您的 UOS App ID
- UOS_APP_SECRET: 您的 UOS App Secret
- UOS_APP_SERVICE_SECRET: 您的 UOS App Service Secret

点击「立即测试配置」,并在测试成功后,点击「立即启用配置」。

3.3.开启第一台服务器
要使用 Func Realtime 提供的接口功能,您需要确保至少有一台服务器可用。点击「启动服务器」,填入服务器名称及您可能需要的标签数据,点击「启动」。
3.4.启用缩扩容机制(可选)
弹性伸缩功能可以根据您配置的策略,为您自动监测服务器状态,并分配空闲服务器。
4.核心类&接口
MuninnNetwork 接口
MuninnNetwork 是客户端所有发起交互的入口。
public class MuninnNetwork
{
// 初始化房间信息并加入服务器
public static async Task CreateOrJoinRoom(CreateOrJoinRoomRequest request = null);
// 列举所有服务器信息(不包含房间信息)
public static async Task<Server[]> ListServers()
// 加入一台可用的服务器(需要该服务器上已初始化房间信息)
public static async Task JoinRoom(JoinRoomRequest request = null);
// 主动离开房间
public static void LeaveRoom();
// 发送消息
public static void RaiseEvent(byte[] data, RaiseEventOptions options);
// 订阅兴趣组
// 兴趣组范围是[1, 255]之间, 0 是系统默认保留(不需要订阅)
public static void SubscribeGroups(List<uint> groups);
// 退订兴趣组
public static void UnsubscribeGroups(List<uint> groups);
// 添加 CachedEvent
public static void AddCachedEvent(string tag, byte[] data, Action<MuninnAddCachedEventResponse> callback);
// 移除 CachedEvent
public static void RemoveCachedEvent(string tag, Action<MuninnRemoveCachedEventResponse> callback);
// 设置 StickyEvent
public static void SetStickyEvent(string key, byte[] data, StickyEventScope scope, Action<MuninnSetStickyEventResponse> callback);
// 移除 StickyEvent
public static void RemoveStickyEvent(string key, StickyEventScope scope, Action<MuninnRemoveStickyEventResponse> callback);
// 查询 StickyEvent
public static void GetStickyEvent(string key, StickyEventScope scope, Action<MuninnGetStickyEventResponse> callback);
// 远程调用 MasterClient 上的服务端逻辑
public static void ServerCall(byte[] data);
// 踢走玩家
public static void KickPlayer(uint senderId);
// 更新玩家的属性
public static void UpdatePlayerInfo(MuninnPlayer player, Action<MuninnUpdatePlayerResponse> callback);
// 更新房间自定义属性
public static void UpdateRoomCustomProperties(Dictionary<string, string> properties, Action<MuninnUpdateRoomCustomPropertiesResponse> callback);
}MuninnBehaviour 接口
MuninnBehaviour 是客户端所有接收到交互信息的出口。开发者需要根据自己游戏逻辑的需要去实现这些方法。
public class MuninnBehaviour : MonoBehaviour
{
// 成功加入房间
public virtual void OnJoinedRoom(MuninnRoomView roomView);
// 加入房间失败
public virtual void OnJoinRoomFailed(MuninnError error);
// 有新玩家进入房间时通知
public virtual void OnPlayerEnteredRoom(MuninnPlayer player);
// 有玩家离开房间时通知
public virtual void OnPlayerLeftRoom(MuninnPlayer player);
// 成功离开房间
public virtual void OnLeftRoom(LeaveRoomEvent leaveRoomEvent);
// 连接关闭
public virtual void OnDisconnected();
// 收到消息
public virtual void OnEvent(MuninnEvent muninnEvent);
// 成功订阅兴趣组
public virtual void OnSubscribeGroups(MuninnSubscribeGroupResponse rsp);
// 订阅兴趣组失败
public virtual void OnSubscribeGroupsFailed(MuninnError error);
// 成功退订兴趣组
public virtual void OnUnsubscribeGroups(MuninnUnsubscribeGroupsResponse rsp);
// 退订兴趣组失败
public virtual void OnUnsubscribeGroupsFailed(MuninnError error);
// 收到Server Call请求
public virtual void OnServerCall(MuninnEvent e);
// MasterClient发生变化
public virtual void OnMasterClientChanged(uint masterClientId);
// 成功踢走玩家
public virtual void OnKickedPlayer(MuninnKickPlayerResponse rsp);
// 踢走玩家失败
public virtual void OnKickPlayerFailed(MuninnError error);
// 玩家属性更新通知
public virtual void OnPlayerInfoUpdated(MuninnPlayer player);
// 房间自定义属性更新通知
public virtual void OnRoomCustomPropertiesUpdated(Dictionary<string, string> customProperties);
}MuninnRoomView(房间动态信息) 接口
MuninnRoomView 维护了房间的基本信息+动态信息(只读)。
public class MuninnRoomView
{
// 房间的静态数据
public MuninnRoom Room { set; get;}
// 房间的动态数据
// 自己在房间内的唯一ID
public uint SenderId {get;}
// 房间内MasterClient的ID
public uint MasterClientId {get;}
// 自己的信息
public MuninnPlayer Self() {get;}
// 自己是不是MasterClient
public bool IsMasterClient();
// 房间里的在线用户
public List<MuninnPlayer> Players {get;}
public MuninnPlayer GetPlayerBySenderId(uint senderId);
public MuninnPlayer GetPlayerByUniqueId(string uniqueId);
// 获取CachedEvent列表
public List<MuninnCachedEvent> RoomCachedEvents {get;}
// 获取自己订阅过的群组
public List<uint> MyGroups {get;}
}5.集成示例
MuninnBehaviour
public partial class MuninnBehaviour : MonoBehaviour
{
// 子类覆盖时,需要调用 base.Awake
public virtual void Awake()
{
SetupMuninnCallbacks();
}
// 子类覆盖时,需要调用 base.Update
public virtual void Update()
{
MuninnUpdate();
}
// 子类覆盖时,需要调用 base.OnDestroy
public virtual void OnDestroy()
{
// ...
}
}
// 继承MuninnBehaviour类
class MultiPlayControllerSimple : MuninnBehaviour
{
}配置信息并连接
class MultiPlayControllerSimple : MuninnBehaviour
{
public void Start()
{
// 支持WebSocket/WebSocketSecure/KCP/UTP, 默认为UTP
MuninnSettings.TransportType = MuninnTransportType.WebSocket;
// 开启开发模式, 默认不开启。
// 开启后屏蔽客户端&服务端的心跳检测, 此时建议Transport选择WebSocket/WebSocketSecure
MuninnSettings.Development = true;
// 设置本地玩家的信息
MuninnNetwork.PlayerInfo = new MuninnPlayerInfo()
{
Id = "<playerUniqueId>",
Name = "<playerName>",
Properties = new Dictionary<string, string>
{
["key1"] = "value1",
["key2"] = "value2",
},
};
}
}重载事件回调
class MultiPlayControllerSimple : MuninnBehaviour
{
public override void OnJoinedRoom(MuninnRoomView roomView)
{
// 1. 加入房间后需要处理的信息
// 房间信息
MuninnRoom curRoom = roomView.Room;
// 玩家自己的信息
MuninnPlayer owner = roomView.Self();
// 房间的在线用户列表(包含自己)
List<MuninnPlayer> players = roomView.Players;
// 加入房间时,附带的CachedEvent列表,帮助游戏快速恢复快照等等
List<MuninnCachedEvent> cachedEvents = roomView.CachedEvents;
// 2. 其他信息
// 房间给该玩家分配的Id (保证唯一)
uint senderId = roomView.SenderId;
// MasterClient对应的Id
uint masterClientId = roomView.MasterClientId;
// 快速判断自己是不是MasterClient
roomView.IsMasterClient();
}
public override void OnPlayerEnteredRoom(MuninnPlayer player)
{
Debug.LogFormat("[Muninn] Player {0} entered room", player.Id);
}
public override void OnPlayerLeftRoom(MuninnPlayer player)
{
Debug.LogFormat("[Muninn] Player {0} left room", player.Id);
}
public override void OnLeftRoom(LeaveRoomEvent e)
{
LeaveRoomReason reason = e.reason;
Debug.LogFormat("[Muninn]: OnLeftRoom() was called by Muninn. Reason : {0}", reason);
switch (reason)
{
case LeaveRoomReason.DisconnectByClient:
break;
case LeaveRoomReason.DisconnectByKick:
break;
case LeaveRoomReason.DisconnectByTimeout:
break;
case LeaveRoomReason.DisconnectByCloseRoom:
break;
}
}
public override void OnEvent(MuninnEvent e)
{
Debug.Log("[Muninn] OnEvent() : " + System.Text.Encoding.Default.GetString(e.Data));
}
}发送消息接口
var content = "Hello";
// 1.普通消息
MuninnNetwork.RaiseEvent(
Encoding.UTF8.GetBytes(content),
new RaiseEventOptions() {Target = RaiseEventTarget.TO_ALL}
);
// 2.群组消息
MuninnNetwork.RaiseEvent(
Encoding.UTF8.GetBytes(content),
new RaiseEventOptions() {
Target = RaiseEventTarget.TO_GROUPS,
TargetGroups = new List<uint> {1, 2, 3}.ToArray(),
}
);
// 3.ServerCall
MuninnNetwork.ServerCall(Encoding.UTF8.GetBytes(content));兴趣组接口
List<uint> groups = new List<uint>() { 1, 2, 3 };
// 1.订阅兴趣组
// 兴趣组范围是[1, 255]之间, 0 是系统默认保留(不需要订阅)
MuninnNetwork.SubscribeGroups(groups);
// 2.退订兴趣组
MuninnNetwork.UnsubscribeGroups(groups);CachedEvent接口
string tag = "<cached-event-tag>";
byte[] data = Encoding.UTF8.GetBytes("<cached-event-data>");
// 1.添加CachedEvent
Action<MuninnAddCachedEventResponse> callback = (r) =>
{
if (r.Code == (uint)MuninnCode.OK)
{
Debug.LogFormat("AddCachedEvent Successfully");
}
else
{
MuninnError err = MuninnStatusCodeHelper.Convert(r.Code);
Debug.LogFormat("AddCachedEvent Fail, err:{0}, msg: {1}", err.Code, err.Description);
}
};
MuninnNetwork.AddCachedEvent(tag, data, callback);
// 2.移除CachedEvent
Action<MuninnRemoveCachedEventResponse> callback = (r) =>
{
if (r.Code == (uint)MuninnCode.OK)
{
Debug.LogFormat("RemoveCachedEvent Successfully");
}
else
{
MuninnError err = MuninnStatusCodeHelper.Convert(r.Code);
Debug.LogFormat("RemoveCachedEvent Fail, err:{0}, msg: {1}", err.Code, err.Description);
}
};
MuninnNetwork.RemoveCachedEvent(tag, callback);StickyEvent接口
string key = "<sticky-event-key>";
byte[] data = Encoding.UTF8.GetBytes("<sticky-event-data>");
StickyEventScope scope = StickyEventScope.Room;
// 1.添加StickyEvent
Action<MuninnSetStickyEventResponse> callback = (r) =>
{
if (r.Code == (uint)MuninnCode.OK)
{
Debug.LogFormat("SetStickyEvent Successfully");
}
else
{
MuninnError err = MuninnStatusCodeHelper.Convert(r.Code);
Debug.LogFormat("SetStickyEvent Fail, err:{0}, msg: {1}", err.Code, err.Description);
}
};
MuninnNetwork.SetStickyEvent(key, data, scope, callback);
// 2.移除StickyEvent
Action<MuninnRemoveStickyEventResponse> callback = (r) =>
{
if (r.Code == (uint)MuninnCode.OK)
{
Debug.LogFormat("RemoveStickyEvent Successfully");
}
else
{
MuninnError err = MuninnStatusCodeHelper.Convert(r.Code);
Debug.LogFormat("RemoveStickyEvent Fail, err:{0}, msg: {1}", err.Code, err.Description);
}
};
MuninnNetwork.RemoveStickyEvent(key, scope, callback);
// 3.查询StickyEvent
Action<MuninnGetStickyEventResponse> callback = (r) =>
{
if (r.Code == (uint)MuninnCode.OK)
{
Debug.LogFormat("GetStickyEvent Successfully");
}
else
{
MuninnError err = MuninnStatusCodeHelper.Convert(r.Code);
Debug.LogFormat("GetStickyEvent Fail, err:{0}, msg: {1}", err.Code, err.Description);
}
};
MuninnNetwork.GetStickyEvent(key, scope, callback);用户属性接口
//1. 更新用户
Action<MuninnUpdatePlayerResponse> callback = (r) =>
{
if (r.Code == (uint)MuninnCode.OK)
{
Debug.LogFormat("UpdatePlayer Successfully");
}
else
{
MuninnError err = MuninnStatusCodeHelper.Convert(r.Code);
Debug.LogFormat("UpdatePlayer Fail, err:{0}, msg: {1}", err.Code, err.Description);
}
};
MuninnNetwork.UpdatePlayerInfo(new MuninnPlayer() {
SenderId = 2, // 房间分配的短id
Name = "<name>",
Properties = new Dictionary<string, string>(),
}, callback);
//2. 踢除用户(仅允许房主操作!)
uint senderId = 2; // 房间分配的短id
MuninnNetwork.KickPlayer(senderId);
//3. 更新房间自定义属性
Dictionary<string, string> customProperties = new Dictionary<string, string>()
{
{"key1", "val1"},
{"key2", "val2"},
};
Action<MuninnUpdateRoomCustomPropertiesResponse> callback = (r) =>
{
if (r.Code == (uint)MuninnCode.OK)
{
Debug.LogFormat("UpdateRoomCustomProperties Successfully");
}
else
{
MuninnError err = MuninnStatusCodeHelper.Convert(r.Code);
Debug.LogFormat("UpdateRoomCustomProperties Fail, err:{0}, msg: {1}", err.Code, err.Description);
}
};
MuninnNetwork.UpdateRoomCustomProperties(customProperties, callback);6.SDK错误码
// 服务端的错误[10000, INF)
OK = 0,
InvalidParam = 10000,
InternalServerError = 10001,
OperationFailed = 10002,
MaxPlayersExceeded = 20001,
MismatchedAppId = 20002,
InvalidJoinCode = 20003,
PlayerJoinedAnotherRoom = 20004,
ConnectionFailed = 20099,
UserReLoggedIn = 20101,
RoomClosed = 20102,
UnexpectedRoomError = 20103,
ClientTimedOut = 20104,
KickPlayerByMasterClient = 20105,
KickPlayerFailForSelf = 20106,
KickPlayerFailForNotFound = 20107,
KickPlayerFailForNonMasterClient = 20108,
UpdatePlayerFailForOffline = 20201,
UpdatePlayerFailForPermissionDenied = 20202,
// StickyEvent相关的错误码
MaxStickyEventsExceeded = 20300,
MismatchedStickyEventKey = 20301,
StickyEventKeyNotFound = 20302,
// 客户端定义的错误码[1, 10000)
InvalidClientAppId = 1000,
InvalidClientAppSecret = 1001,
InvalidClientPlayerId = 1002