美文网首页程序员
云开发让 Unity 微信小游戏实时聊起来

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

作者: 蛋先生DX | 来源:发表于2024-09-17 10:55 被阅读0次

    写在最前

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

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

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

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

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

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

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

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

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

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

    蛋先生:直说吧,啥问题

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

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

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

    Unity 如何用上云数据库?

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

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

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

    蛋先生:那你先去撸代码

    1.jpeg

    丹尼尔:蛋兄,搞不定 (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,大致信息如下:

    2.png

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

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

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

    接收消息

    蛋先生:假设你的用户名为 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
    代码有点粗糙,仅供参考,还望见谅!

    相关文章

      网友评论

        本文标题:云开发让 Unity 微信小游戏实时聊起来

        本文链接:https://www.haomeiwen.com/subject/bolgljtx.html