写点什么

云开发让 Unity 微信小游戏实时聊起来

作者:蛋先生DX
  • 2024-09-17
    广东
  • 本文字数:7534 字

    阅读完需:约 25 分钟

云开发让 Unity 微信小游戏实时聊起来

写在最前

本故事是《How Can Unity+腾讯云开发=微信小游戏?》的续篇,主要聊的是在使用 Unity 开发微信小游戏过程中,如何使用云开发来给小游戏增添一抹实时互动的亮色(比如实时聊天)


温馨提示:各家的云开发功能各具特色,本文的云开发特指腾讯云云开发

云开发,哪个服务可实现实时聊天?

丹尼尔:蛋兄,我又来了。上次跟你聊完(请看上集《How Can Unity+腾讯云开发=微信小游戏?》)后,我已经在 Unity 微信小游戏中用上云开发的数据模型了,云函数也顺手捎上了


蛋先生:不错,挺速度的嘛


丹尼尔:这些一来一回的后端接口,使用数据模型和云函数,唰唰唰一下就搞定了,别提多爽


蛋先生:是的,对于后端接口的搭建,这些服务确实可以大大简化你的工作,让你聚焦你的业务


丹尼尔:但我现在又遇到问题了


蛋先生:我就知道,无事不登三宝殿


丹尼尔:瞧您说的,主要是来看看您,顺便问下问题啦 (′▽`〃)


蛋先生:直说吧,啥问题


丹尼尔:我的小游戏里,玩家之间是可以聊天的,但我没发现云开发有 WebSocket 相关的服务


蛋先生:据我所知,云开发目前是没有提供这种纯粹的服务的。但是,云数据库有实时推送的功能,用它来实现你的需求应该是木有问题的


丹尼尔:啊~,在云数据库这啊,藏得够深的,How?

Unity 如何用上云数据库?

蛋先生:首先,咱们得让 Unity 能用上云数据库,你需要……


(丹尼尔打断了蛋先生的讲话)


丹尼尔:我懂我懂,这跟《How Can Unity+腾讯云开发=微信小游戏?》提到的数据模型是一个套路的


蛋先生:那你先去撸代码



丹尼尔:蛋兄,搞不定 (o_ _)ノ。这云数据库的 API 不像数据模型那么简单,我实在想不出如何用一个万能 JS 函数搞定


蛋先生:咳咳~。那咱们先把云数据库增删查改的调用示例整理出来,如下


var db = app.database();
db.collection("hello").add({...})db.collection("hello").doc("...").remove()db.collection("hello").where({...}).remove()db.collection("hello").doc("...").get()db.collection("hello").where({...}).get()db.collection("hello").get()db.collection("hello").doc("...").update({...})db.collection("hello").doc("...").set({...})db.collection("hello").where({...}).update({...})
复制代码


你看出什么门道了没?


丹尼尔:都有 collection?都是链式调用?


蛋先生:说到重点了,链式调用。链式调用就像是一串糖葫芦,一步接一步:方法名,入参,方法名,入参...


丹尼尔:然后呢?


蛋先生:根据这个规律,我们可以定一个 chainList 入参来实现 JS 函数,每一项就是一个方法名和方法入参。代码如下


Database_API: async function (callbackId, params) {    ...    const { collectionName, chainList } = asmLibraryArg        .Utils()        .parseInputParams(params);    ...    let db;
if (platform === constants.PLATFROM.WX) { db = wx.cloud.database(); } else if (platform === constants.PLATFROM.WEB) { db = app.database(); }
let chainObj = db.collection(collectionName); chainList.forEach((chainItem) => { const method = chainItem.method; const optionsStr = chainItem.optionsStr; let options = optionsStr ? JSON.parse(optionsStr) : ""; ... chainObj = chainObj[method](options); }); const data = await chainObj; asmLibraryArg.Utils().sendMessage(callbackId, data.data || data);}
复制代码


丹尼尔:你他 * 的真是个人才


蛋先生:夸人可以,但要文明


丹尼尔:嘻嘻,接下来就是 Unity 实现了


蛋先生:我们可以把刚刚整理的调用示例发给 GPT,让它帮咱们生成初步的接口定义和类实现,我们再调整一下即可。大概的 Prompt 如下


JS 是这么调用的var db = app.database();db.collection("hello").add({})...
我希望在 Unity 也能这样调用,请帮我设计相应的类或接口
复制代码


丹尼尔:可以啊,AI 用得溜溜的


蛋先生:基操而已。接下来我们来填补真正的实现细节


丹尼尔:好咧~


(温馨提醒:请参考下边的【代码块一】进行阅读)


蛋先生:对于每一个链式调用,我们只需实现最后的方法


比如 db.collection("hello").where({...}).get(),要填补实现的方法就是 QueryHandlerGet<T> 方法


而它的实现仅仅是提供 collection 名称(collectionName)和链式调用的方法名和入参(chainList)


公共逻辑实现 CommonHandler 跟数据模型的实现基本一致,这里就不作赘述


//【代码块一】
private class Database : IDatabase{ public ICollection Collection(string name) => new CollectionHandler(name);
private static async Task<T> CommonHandler<T>(DatabaseAPIParam param) { (string, TaskCompletionSource<string>) asyncTask = Internal.GetAsyncTask();
Internal.Database_API(asyncTask.Item1, JsonConvert.SerializeObject(param));
string result = await asyncTask.Item2.Task; return Internal.ParseOutputResult<T>(result); }
public class CollectionHandler : ICollection { private readonly string collectionName; public CollectionHandler(string name) { collectionName = name; }
... public IQuery Where(object filter) => new QueryHandler(collectionName, filter); ... }
...
public class QueryHandler : IQuery { private string collectionName; private object filter; public QueryHandler(string collectionName, object filter) { this.collectionName = collectionName; this.filter = filter; }
public Task<T> Get<T>() { return CommonHandler<T>(new DatabaseAPIParam() { collectionName = collectionName, chainList = new[] { new ChainItem() { method = "where", optionsStr = JsonConvert.SerializeObject(filter) }, new ChainItem() { method = "get", optionsStr = "" } } }); } ... }
}
private class ChainItem{ public string method { get; set; } public string optionsStr { get; set; }}private class DatabaseAPIParam{ public string collectionName { get; set; } public ChainItem[] chainList { get; set; }}
复制代码

实时推送 Watch,需要重点讲讲

丹尼尔:云数据库这种一来一回的模式,被你这么一说,对接起来还是挺简单的。然而到现在,实时推送还没有呢


蛋先生:实时推送的对接有点不一样,我们先来看下 JS 的调用示例


var db = app.database();
const watcher = db .collection("hello") .where({ // query... }) .watch({ onChange: function (data) { ... }, onError: function (err) { ... } }); // watcher.close()
复制代码


丹尼尔:恩,请把"有点"去掉,谢谢


蛋先生:为了更好地理解,我们要从实时推送的生命周期说起。以下是对应 JS 版本的在 Unity 调用 Watch 的代码


var watchObj = database.Collection("hello").Where(new Dictionary<string, object>{    // query...}).Watch(new WatchParams<ModelHello>(){    OnChange = (WatchChangeData<ModelHello> data) =>    {        ...    },    OnError = (string err) =>    {        ...    }});
复制代码


丹尼尔:接下来又是一大波让人头疼的代码片段吗?(>人<;)


蛋先生:嘿嘿,代码是不可避免的,依然需要结合下边代码【脚本 C】和【脚本 J】来看(温馨提示:【脚本 C】和【脚本 J】为往下一点点的两个大代码块)

连接的建立

丹尼尔:Come on,我已经准备好了!


蛋先生:【脚本 C】中的 Watch<T> 方法是一切的开始


public IWatchObj Watch<T>(WatchParams<T> param)
复制代码


首先,我们获取 uuid,作为 JS 与 Unity 沟通的凭证


然后,实例化一个 WatchObj 对象,并把它保存在 watchDictionary 字典中,以备后用


接着,调用 Database_API JS 方法


最后,把 WatchObj 返回


丹尼尔:我注意到 watch 的入参是 action = open


蛋先生:眼力不错。这里设计了入参 action,是为了可以支持多种行为(当前只需支持 open 和 close)


丹尼尔:好,请继续!


蛋先生:紧接着就到了 Database_API JS 方法这。【脚本 J】中加了个分支逻辑(通过判断链式调用最后的方法名是否为 watch)来处理 watch 行为,即调用云数据库的 watch API,这样连接就建立上了。我们利用 JS 函数也是对象的特性,将 watch 对象同样保存起来,后续 close 的实现就靠它了

消息的接收

丹尼尔:Nice,请继续!


蛋先生:好嘞!我们通过 onChange 和 onError 这两位侦探,来监听消息(正常消息和异常消息一个不落)。只要有风吹草动,它们就会通过 SendMessage 去通知 Unity。


丹尼尔:那 Unity 在哪接收消息呢?


蛋先生:依然在 OnAsyncFnCompleted。我们在 callbackId 上动了点手脚,增加了分类信息。比如说,"watch_" 开头的,就是专门为 watch 类型的。


丹尼尔:我刚刚就好奇 string uuid = "watch_" + Guid.NewGuid().ToString(); 这里的 uuid 生成规则,现在解惑了


蛋先生:恩,最后,我们通过 watchObj 的 PerformXXXAction 来触发具体事件的执行。这就完成了整个消息监听的流程了

连接的关闭

丹尼尔:关闭应该就是通过 watchObj 的 close 方法了


蛋先生:没错。具体就是通过 action = close 去通知 JS 执行实际的关闭逻辑了


//【脚本C】
public class TCBSDK : MonoBehaviour{
private class Database : IDatabase { ...
public class QueryHandler : IQuery { ...
public IWatchObj Watch<T>(WatchParams<T> param) {
string uuid = "watch_" + Guid.NewGuid().ToString(); WatchObj cls = new(uuid, (string data) => param.OnChange(JsonConvert.DeserializeObject<WatchChangeData<T>>(data)), (string data) => param.OnError(JsonConvert.DeserializeObject<string>(data))); Internal.watchDictionary.Add(uuid, cls); Internal.Database_API(uuid, JsonConvert.SerializeObject(new DatabaseAPIParam() { collectionName = collectionName, chainList = new[] { new ChainItem() { method = "where", optionsStr = JsonConvert.SerializeObject(filter) }, new ChainItem() { method = "watch", optionsStr = JsonConvert.SerializeObject(new Dictionary<string, string>{ ["action"] = "open" }) } } })); return cls; } } ... } private class Internal { public static readonly Dictionary<string, WatchObj> watchDictionary = new(); ... }
...
private class WatchObj : IWatchObj { ...
public WatchObj(string callbackIdInput, OnWatchHandler<string> changeCallback, OnWatchHandler<string> errorCallback) { callbackId = callbackIdInput; OnChange += changeCallback; OnError += errorCallback; }
public void Close() { Internal.Database_API(callbackId, JsonConvert.SerializeObject(new DatabaseAPIParam() { chainList = new[] { new ChainItem() { method = "watch", optionsStr = JsonConvert.SerializeObject(new Dictionary<string, string>{ ["action"] = "close" }) }, } })); Internal.watchDictionary.Remove(callbackId); }
public void PerformChangeAction(string msg) { OnChange?.Invoke(msg); }
public void PerformErrorAction(string err) { OnError?.Invoke(err); } }

public void OnAsyncFnCompleted(string result) { AsyncResponse<string> res = Internal.ParseOutputResult<AsyncResponse<string>>(result);
if (res.callbackId.StartsWith("watch_")) { var resultData = Internal.ParseOutputResult<Dictionary<string, object>>(res.result); if (resultData.ContainsKey("err")) { Internal.watchDictionary[res.callbackId].PerformErrorAction(resultData["err"] as string); } else { Internal.watchDictionary[res.callbackId].PerformChangeAction(JsonConvert.SerializeObject(resultData["data"])); }
} else { ... } }
}
复制代码


//【脚本J】
Database_API: async function (callbackId, params) { callbackId = UTF8ToString(callbackId); const { collectionName, chainList } = asmLibraryArg .Utils() .parseInputParams(params); ...
let lastItem = chainList[chainList.length - 1]; if (lastItem.method === "watch") { // watch 的特殊处理
const { action } = JSON.parse(lastItem.optionsStr); if (action === "open") { // 启动 watch
chainList.forEach((chainItem) => { const method = chainItem.method; const optionsStr = chainItem.optionsStr; if (method === "watch") { chainObj = chainObj.watch({ onChange: function (data) { ... asmLibraryArg.Utils().sendMessage(callbackId, { data }); }, onError: function (err) { asmLibraryArg.Utils().sendMessage(callbackId, { err }); }, }); } else { chainObj = chainObj[method]( optionsStr ? JSON.parse(optionsStr) : "" ); } }); asmLibraryArg.Database_API[callbackId] = chainObj; } else if (action === "close") { // 关闭 watch
if (asmLibraryArg.Database_API[callbackId]) { asmLibraryArg.Database_API[callbackId].close(); delete asmLibraryArg.Database_API[callbackId]; } } } else { // 普通异步接口调 ... }}
复制代码

如何用实时推送完成实时聊天

丹尼尔:这下终于可以用上云数据库的实时推送了,那么具体怎么实现实时聊天呢?


蛋先生:好问题,实时推送是靠监听云数据库的数据变化来实现的。所以我们得先给聊天消息建一个数据模型 chat_message,大致信息如下:



丹尼尔:等等,不是说要用云数据库吗?怎么变成了数据模型了?


蛋先生:数据模型其实是云数据库的简化版本,底层仍然是云数据库


丹尼尔:哦,原来如此!您继续

接收消息

蛋先生:假设你的用户名为 Daniel,你在和 Tom 聊天。那么要接收 Tom 发给你的消息,可以按 from 和 to 这两个条件去查询,如下


// 接收消息
var database = app.Database();
var watchObj = database.Collection("chat_message").Where(new Dictionary<string, object>{ ["from"] = "Tom", ["to"] = "Daniel"}).Watch(new WatchParams<ModelChatMessage>(){ OnChange = (WatchChangeData<ModelChatMessage> data) => { if (data.type != "init") { Debug.Log($"接收到的消息:{JsonConvert.SerializeObject(data.docChanges)}"); } }, OnError = (string err) => { Debug.Log($"watch err: {err}"); }});
复制代码


这样当有符合查询条件的数据插入时,你就会实时收到插入的数据信息了

发送消息

丹尼尔:懂了!发送消息应该就是插入一条数据咯,如下


await app.Models.Create<ModelsCreateRes>(new ModelsReqParams() {     modelName = "chat_message",     options = new Dictionary<string, object>    {        ["data"] = new Dictionary<string, string>        {            ["from"] = "Daniel",  // 发送人            ["to"] = "Tom",  // 接收方            ["content"] = "Hi man"  // 消息内容        }    } });
复制代码


蛋先生:很好!接下来就是你的自由发挥时间了


以上完整代码请移步到仓库:https://github.com/daniel-dx/unity-cloudbase-demo

代码有点粗糙,仅供参考,还望见谅!

发布于: 刚刚阅读数: 3
用户头像

蛋先生DX

关注

一源一世界 2019-03-03 加入

我是只会用大白话来聊技术的蛋先生DX ncform / ncgen / nice-hooks 开源项目作者

评论

发布
暂无评论
云开发让 Unity 微信小游戏实时聊起来_腾讯云_蛋先生DX_InfoQ写作社区