How Can Unity+騰訊云開發(fā)=微信小游戲?

banner2.jpg

寫在最前

時光飛逝,物是人非

技術(shù)方案總是帶著時代的標(biāo)簽,曾經(jīng)前沿的技術(shù),經(jīng)過時間的洗禮,可能已成為經(jīng)典

如果你點進(jìn)來是因?qū)嶋H需求而不是隨意瞧瞧,請同時看看官方的最新進(jìn)展,指不定有意外的收獲

本故事主要講解在使用 Unity 開發(fā)微信小游戲時,如何第一時間用上騰訊云開發(fā)的新能力,以及可能的最佳開發(fā)實踐


故事背景

蛋先生:丹尼爾,好久不見,怎么愁眉苦臉的?

丹尼爾:蛋兄好,最近在折騰用 Unity 開發(fā)微信小游戲,服務(wù)用的是騰訊云開發(fā),不過碰到些問題,一時半會兒搞不定

蛋先生:哦?什么問題,說來聽聽

丹尼爾:蛋兄,你也懂 Unity 嗎?

蛋先生:略懂略懂

丹尼爾:如此甚好。我起初以為直接調(diào)用騰訊云開發(fā),用官方的 Unity SDK 就行了

蛋先生:恩,這個想法沒毛病

丹尼爾:但,官方并沒有 Unity 版本的 SDK

蛋先生:然后呢?

丹尼爾:后來,我在將 Unity 轉(zhuǎn)成微信小游戲的過程中,發(fā)現(xiàn)微信有提供 WeChatWASM 這個 Unity SDK,里面帶了個 WX.cloud

蛋先生:那不就解了嗎?

丹尼爾:我也天真地這么以為,可在 Unity 編輯器一運(yùn)行,就...

1.png

蛋先生:這也正常,畢竟 Unity 編輯器又不是微信開發(fā)的,Play Mode 下沒有微信小游戲運(yùn)行環(huán)境,自然是跑不起來的

丹尼爾:所以我需要寫代碼,構(gòu)建成 WebGL,再轉(zhuǎn)換成微信小游戲,最后才能在微信開發(fā)者工具看到運(yùn)行效果。而我運(yùn)氣不好,還遇到了開發(fā)者工具罷工,還得多走一步,用真機(jī)預(yù)覽。這一個流程下來感覺要 1 分鐘左右,每次改代碼都得走這一長征路,想想頭就大

2.png

蛋先生:會不會是使用姿勢不對呢?

丹尼爾:我也懷疑過,直到我看到官方文檔介紹的開發(fā)方式...

3.png

蛋先生:看來只能如此,不過對于微信本身的能力,大部分時間我們不需要怎么消費它的輸出,好像問題也不大。比如:

var bannerAd = WX.CreateBannerAd(new WXCreateBannerAdParam()
{
    adUnitId = "xxxx",
    adIntervals = 30,
    style = new Style()
    {
        left = 0,
        top = 0,
        width = 600,
        height = 200
    }
});

bannerAd.OnLoad(()=> {
    bannerAd.Show();
});
bannerAd.OnError((WXADErrorResponse res)=>
{
    Debug.Log(res.errCode);
});

丹尼爾:恩,這倒是。但對于服務(wù)接口,我們是重度消費輸出數(shù)據(jù)的,比如排行榜,好友列表等。總不能來回盲寫代碼,走一遍長征路調(diào)試吧

蛋先生:這長征路是難免的,但可以減少次數(shù)。比如在 Unity 編輯器開發(fā)時使用 Mock 數(shù)據(jù),等業(yè)務(wù)邏輯走通再走長征路

丹尼爾:我還是希望盡早看到集成了云開發(fā)服務(wù)后的實際效果,這樣可以早點發(fā)現(xiàn)問題,減少在長征路上浪費的時間

蛋先生:恩...

丹尼爾:還有個問題,WeChatWASM 對云開發(fā) SDK 的支持,存在滯后的問題,當(dāng)前只支持 CallFunction,不支持最新的數(shù)據(jù)模型。我就是沖著騰訊云開發(fā)剛新鮮出爐不久的數(shù)據(jù)模型來的

蛋先生:哦...

丹尼爾:煩死了,這也不行,那也不行,蛋兄,你給出出主意唄

蛋先生:自己搞一個吧

丹尼爾:這,蛋兄,你不是開玩笑的吧?這可不是我的主業(yè)務(wù),我只想用云開發(fā)服務(wù)而已

蛋先生:恩,可以搞,但不能瞎搞。我們來分析一下要解決的核心問題:

一 要能用上云開發(fā)的最新功能,比如數(shù)據(jù)模型;

二 爭取可以早點看到實際效果,避免走長征路;

三 實現(xiàn)成本要低,畢竟這不是主業(yè)務(wù)

丹尼爾:恩,那怎么辦?

解決用上云開發(fā)最新功能的問題

蛋先生:云開發(fā)推出新功能,總是第一時間在 JS 環(huán)境(包括云函數(shù),小程序和 Web 頁面)提供的,對吧

4.png
5.png

丹尼爾:沒錯

蛋先生:所以我們實際應(yīng)該直接調(diào)用這些 SDK,在 Web 環(huán)境下就調(diào)用 js-sdk,在小游戲環(huán)境下就調(diào)用小程序 sdk,這樣就能確保享受到最新的功能了

丹尼爾:這些是運(yùn)行在 JS 環(huán)境的吧

蛋先生:沒錯!所以我們需要實現(xiàn)一個包裹層,這個包裹層對內(nèi)使用 Unity 腳本去調(diào)用 Javascript 函數(shù),對外提供云開發(fā)的 Unity 版本 SDK

6.png

丹尼爾:具體怎么實現(xiàn)呢?

蛋先生:實現(xiàn)細(xì)節(jié)咱們后面再講

解決少走長征路,盡早看到實際效果的問題

丹尼爾:好吧,我還有一個問題,為什么要考慮 Web 環(huán)境呢?我只想開發(fā)微信小游戲而已啊

蛋先生:這就是為了解決第二個核心問題 - 縮短調(diào)試路徑。雖然我們在 Unity 編輯器的 Play Mode 無法預(yù)覽效果,但我們可以退而求其次,構(gòu)建成 WebGL。當(dāng)你開發(fā)完實際的調(diào)用云開發(fā)的代碼后,按 Command + B ,這會構(gòu)建成 WebGL 應(yīng)用,并自動在瀏覽器打開,你就可以直接預(yù)覽實際效果了,這樣可以省去轉(zhuǎn)換成小游戲的步驟,時間上也會節(jié)省不少

丹尼爾:雖然不能在 Unity 編輯器直接預(yù)覽,還是有點小遺憾,但這樣的調(diào)試路徑還是可以接受的

蛋先生:恩,是時候總結(jié)一下這種方案的可能的最佳開發(fā)流程了

【Unity 編輯器開發(fā)階段】:使用 Mock 數(shù)據(jù),這樣可省去構(gòu)建成 WebGL 的時間,同時可確定業(yè)務(wù)邏輯所使用接口的輸入和輸出數(shù)據(jù)結(jié)構(gòu)

【瀏覽器調(diào)試階段】:根據(jù)確定的業(yè)務(wù)邏輯接口,通過 Unity TCB SDK Wrapper 開發(fā)調(diào)用云開發(fā)服務(wù)的代碼,構(gòu)建成 WebGL,預(yù)覽實際的效果

【微信小游戲預(yù)覽階段】:最后直接構(gòu)建成微信小游戲,進(jìn)行最終效果預(yù)覽

7.png

技術(shù)實現(xiàn)細(xì)節(jié)

丹尼爾:哎呦,不錯哦!不過,我現(xiàn)在比較好奇的是怎么實現(xiàn)這個,以及實現(xiàn)的成本咋樣?

蛋先生:那咱們接著繼續(xù)聊

Unity 調(diào)用 Javascript 同步方法

丹尼爾:首先,Unity 怎么調(diào)用 Javascript 方法呢?

蛋先生:分兩種情況,同步和異步。同步方法比較簡單,官方文檔 已經(jīng)很詳細(xì)了,但咱們也來個小示例

首先,定義個要被 Unity 調(diào)用的 JavaScript 方法。我們在 Assets/Plugins 目錄下創(chuàng)建 .jslib 后綴的文件,比如叫 tcbsdk.jslib

// Assets/Plugins/tcbsdk.jslib

const asmLibraryArg = {
  Hello: function () {
    console.log("Hello, world!");
  },
  ...
}
mergeInto(LibraryManager.library, asmLibraryArg);

接著,在 C# 腳本里進(jìn)行映射,比如 Assets/Scripts/DemoSDK.cs

// Assets/Scripts/DemoSDK.cs

public class DemoSDK
{
    [DllImport("__Internal")]
    public static extern void Hello();
}

然后,你就可以通過 DemoSDK.Hello() 調(diào)用 tcbsdk.jslib 里的 Hello 方法了

丹尼爾:看上去挺簡單的,有啥需要注意的嗎?

蛋先生:還真有,tcbsdk.jslib 里的 asmLibraryArg 這個變量的存在是有意義的,且不能修改成其它變量名

丹尼爾:為啥?

蛋先生:它的存在主要是為了方法間互相調(diào)用。來,我們繼續(xù)看例子,看看 HelloCallOtherFn 是怎么調(diào)用 Hello 的

// Assets/Plugins/tcbsdk.jslib

const asmLibraryArg = {
  Hello: function () {
    console.log("Hello, world!");
  },
  HelloCallOtherFn: function () {
    console.log("Call HelloCallOtherFn");
    // 方法間調(diào)用方式一
    _Hello();
    // 方法間調(diào)用方式二
    asmLibraryArg.Hello();
  },
  ...
}
mergeInto(LibraryManager.library, asmLibraryArg);

丹尼爾:asmLibraryArg.Hello(); 這個我懂,但為啥是 _Hello();?明明方法名是 Hello 啊

蛋先生:嘿嘿,jslib 里的方法在構(gòu)建成 WebGL 時都會經(jīng)過加工后合并進(jìn) webgl.wasm.framework.unityweb.js ,這個文件你可以在生成的 WebGL 產(chǎn)物里找到,來看個代碼片段

// webgl.wasm.framework.unityweb.js

var unityFramework = (() => {
    return function (unityFramework) {
        ...
        function _Hello() {
            console.log("Hello, world!");
        }
        function _HelloCallOtherFn() {
            console.log("Call HelloCallOtherFn");
            // 方法間調(diào)用方式一
            _Hello();
            // 方法間調(diào)用方式二
            asmLibraryArg.Hello();
        },  
        ...
        var asmLibraryArg = {
            Hello: _Hello,
            ... 
        }   
        ...
    }
})

看到?jīng)]?真相就在這兒了。不過,我不推薦用 _Hello(),因為這樣你就沒法用編輯器的功能,比如點擊跳轉(zhuǎn)到方法定義。

8.png

我們前面提到變量名必須是 asmLibraryArg,這其實是一種取巧的方式,這樣即可以實現(xiàn)方法間調(diào)用,又可以充分享受編輯器的智能輔助體驗,一箭雙雕

9.png

丹尼爾:蛋兄,你可真是個小機(jī)靈鬼

蛋先生:咳咳~

Unity 調(diào)用 Javascript 異步方法

丹尼爾:那接下來咱們聊聊如何調(diào)用異步方法

蛋先生:異步調(diào)用是個環(huán),咱們得從一次異步方法調(diào)用的整個過程說起

丹尼爾:你說吧,反正那些又臭又長的代碼我是不想看的

蛋先生:嘿嘿,代碼是不可避免的,還得結(jié)合下邊代碼【腳本C】和【腳本J】來看(溫馨提示:【腳本C】和【腳本J】為往下一點點的兩個大的代碼片段)。假設(shè)我們現(xiàn)在要調(diào)用以下異步方法

HelloWithReturnResult result = await DemoSDK.Instance.HelloAsyncFn(new HelloWithInputParams() { name = "daniel666" });

在【腳本C】中,HelloAsyncFn 方法執(zhí)行時,會先通過 GetAsyncTask() 取得這次調(diào)用的 callbackId 和對應(yīng)的 TaskCompletionSource 異步任務(wù),然后調(diào)用 JavaScript 方法,并等待 TaskCompletionSource 任務(wù)的完成。

丹尼爾:那誰來通知 TaskCompletionSource 任務(wù)完成呢?

蛋先生:好問題!看【腳本J】中 HelloAsyncFn 的實現(xiàn)

異步任務(wù)執(zhí)行完后,會執(zhí)行 asmLibraryArg.Utils().sendMessage(callbackId, result);

這個方法實際執(zhí)行的發(fā)送消息代碼是 Module.SendMessage("DemoSDK", "OnAsyncFnCompleted", param),它會通知名為 "DemoSDK" 的 GameObject 去執(zhí)行腳本組件 DemoSDK 的 OnAsyncFnCompleted 方法

丹尼爾:哦哦,我剛才就想問【腳本C】里的這些代碼是干嘛的,原來是為了創(chuàng)建一個 GameObject 來接收消息。

GameObject gameObject = new("DemoSDK");
DemoSDK demoSDK = gameObject.AddComponent<DemoSDK>();

蛋先生:正解!你可真是個小聰明,這么快就看出來了

接下來【腳本C】的 OnAsyncFnCompleted 方法就會收到消息,然后根據(jù) callbackId 讓具體的 TaskCompletionSource 任務(wù)執(zhí)行 SetResult,從而完成任務(wù),這樣整個異步調(diào)用就閉環(huán)了。

丹尼爾:懂了。但為什么是 Module.SendMessage 呢?這個 Module 是從哪兒冒出來的?

蛋先生:真相依然在 webgl.wasm.framework.unityweb.js

// webgl.wasm.framework.unityweb.js

var unityFramework = (() => {
    return function (unityFramework) {
        ...
        var Module = typeof unityFramework != "undefined" ? unityFramework : {};
        ...
        function SendMessage(gameObject, func, param) {
            ...
        }
        Module["SendMessage"] = SendMessage;
        ...
    }
})

丹尼爾:666,簡直洞若觀火

//【腳本C】:Assets/Scripts/DemoSDK.cs

public class DemoSDK : MonoBehaviour
{
    private static readonly Lazy<DemoSDK> _instance = new Lazy<DemoSDK>(() =>
    {
        GameObject gameObject = new("DemoSDK");
        DemoSDK demoSDK = gameObject.AddComponent<DemoSDK>();
        return demoSDK;
    });
    public static DemoSDK Instance => _instance.Value;
    private DemoSDK() { }

    private Dictionary<string, TaskCompletionSource<string>> tcsDictionary = new();

    public async Task<HelloWithReturnResult> HelloAsyncFn(HelloWithInputParams input)
    {
        (string, TaskCompletionSource<string>) asyncTask = GetAsyncTask();

        // 調(diào)用 JavaScript 方法
        Internal.HelloAsyncFn(asyncTask.Item1, Internal.ParseInputParams(input));

        // 返回 Task 讓調(diào)用方等待
        var result = await asyncTask.Item2.Task;
        return Internal.ParseOutputResult<HelloWithReturnResult>(result);
    }

    public void OnAsyncFnCompleted(string result)
    {
        AsyncResponse<string> res = Internal.ParseOutputResult<AsyncResponse<string>>(result);

        tcsDictionary[res.callbackId].SetResult(res.result);
        tcsDictionary.Remove(res.callbackId);
    }

    private (string, TaskCompletionSource<string>) GetAsyncTask()
    {
        string uuid = Guid.NewGuid().ToString();
        TaskCompletionSource<string> tcs = new();
        tcsDictionary.Add(uuid, tcs);
        return (uuid, tcs);
    }

    private class Internal
    {
        [DllImport("__Internal")]
        public static extern void HelloAsyncFn(string callbackId, string input);
    }

    private class AsyncResponse<T>
    {
        public string callbackId { get; set; }
        public T result { get; set; }
    }
}
//【腳本J】:Assets/Plugins/tcbsdk.jslib

const asmLibraryArg = {
  HelloAsyncFn: async function (callbackId, input) {
    ...
    const result = await new Promise((resolve) =>
      setTimeout(() => {
        resolve(input), 2000;
      })
    );
    asmLibraryArg.Utils().sendMessage(callbackId, result);
  },
  Utils: function () {
    const utils = {
      ...
      sendMessage(callbackId, result) {
        const unityInstance = Module;
        const constants = asmLibraryArg.Constants();
        unityInstance.SendMessage(
          constants.DEMO_CALLBACK_OBJECT_NAME,
          constants.CALLBACK_METHOD_NAME,
          JSON.stringify({
            callbackId,
            result: JSON.stringify(result || ""),
          })
        );
      },
    };
    return utils;
  },
  Constants: function () {
    return {
      ...
      DEMO_CALLBACK_OBJECT_NAME: "DemoSDK",
      CALLBACK_METHOD_NAME: "OnAsyncFnCompleted",
    };
  },
  ...
}
mergeInto(LibraryManager.library, asmLibraryArg);

集成騰訊云開發(fā) js-sdk 和小程序 sdk

丹尼爾:說了這么多,好像還不知道要怎么集成 js-sdk 和小程序 sdk

蛋先生:搞清楚了如何實現(xiàn) Unity 調(diào)用 Javascript 之后,問題不就迎刃而解了嘛

因為 @cloudbase/wx-cloud-client-sdk@cloudbase/js-sdk 都提供了 UMD 格式的完整版本,所以我們只需要將 SDK 的完整代碼復(fù)制放到 jslib 里面(請留意 CloudbaseJSSdkScript 和 CloudbaseWXCloudClientSdkScript 方法)即可。以下是 init 的實現(xiàn)示例

// Assets/Plugins/tcbsdk.jslib

const asmLibraryArg = {

  /**
   * params.env: 環(huán)境ID
   */
  CloudInit: async function (callbackId, params) {
    ...
    const input = asmLibraryArg.Utils().parseInputParams(params);
    if (platform === constants.PLATFROM.WX) {
      const { init } = asmLibraryArg.GetCloudbaseWXCloudClientSdkInstance();
      await wx.cloud.init({
        env: input.env,
      });
      const app = init(wx.cloud);
      ...
    } else if (platform === constants.PLATFROM.WEB) {
      const cloudbase = asmLibraryArg.GetCloudbaseJSSdkScriptInstance();
      const app = cloudbase.init({
        env: input.env,
      });
      ...
    }
    asmLibraryArg.Utils().sendMessage(callbackId);
  },

  /**
   * 獲取 @cloudbase/wx-cloud-client-sdk 實例
   */
  GetCloudbaseWXCloudClientSdkInstance: function () {
    const global = asmLibraryArg.GetGlobalData();
    if (!global.wxCloudClientSDK) {
      asmLibraryArg.CloudbaseWXCloudClientSdkScript.call(global);
    }
    return global.wxCloudClientSDK;
  },

  /**
   * 獲取 cloudbase-js-sdk 實例
   */
  GetCloudbaseJSSdkScriptInstance: function () {
    const global = asmLibraryArg.GetGlobalData();
    if (!global.cloudbase) {
      asmLibraryArg.CloudbaseJSSdkScript.call(global);
    }
    return global.cloudbase;
  },

  /**
   * 加載 @cloudbase/wx-cloud-client-sdk
   */
  CloudbaseWXCloudClientSdkScript: function () {
    (function (exports) {
      /**
       * 以下代碼來自于 https://unpkg.com/@cloudbase/wx-cloud-client-sdk@1.2.1/lib/wxCloudClientSDK.umd.js
       */
      ...
    })();
  },

  /**
   * 加載 cloudbase-js-sdk
   */
  CloudbaseJSSdkScript: function () {
    (function (exports) {
      /**
       * 以下代碼來自于 https://static.cloudbase.net/cloudbase-js-sdk/2.7.13-beta.0/cloudbase.full.js
       */
      ...
    })();
  },
};

mergeInto(LibraryManager.library, asmLibraryArg);

丹尼爾:在微信小游戲中并不需要 js-sdk,這樣豈不是會將 js-sdk 打包進(jìn)小游戲里,不太合適吧

蛋先生:沒錯,不過別擔(dān)心,辦法是有滴。在 jslib 定義的方法并非都會打包,只有那些顯示聲明 [DllImport("__Internal")] 的才會打包進(jìn)去,如下

[DllImport("__Internal")]
public static extern void Hello();

所以,我們可以定義一個 Preprocessor Symbols(比如 WEIXINMINIGAME),在準(zhǔn)備轉(zhuǎn)換成小游戲時,可以在構(gòu)建 WebGL 的設(shè)置中提供這個預(yù)定義符號,這樣 CloudbaseJSSdkScript 方法就會忽略掉了

#if !WEIXINMINIGAME
            [DllImport("__Internal")]
            private static extern void CloudbaseJSSdkScript();
#endif

丹尼爾:Nice

云開發(fā)數(shù)據(jù)模型的實現(xiàn)建議

丹尼爾:感覺我現(xiàn)在就可以動手了,想要什么功能就搞什么功能,主動權(quán)在手上的感覺真好。就先拿數(shù)據(jù)模型開刀吧,蛋兄有什么建議嗎?

蛋先生:通過查閱 云開發(fā)數(shù)據(jù)模型 SDK 文檔,我們可以發(fā)現(xiàn)一個規(guī)律,就是所有方法都是同一種固定模式 models.[model name].[api name](JSON input)

models.post.create({...})
models.post.delete({...})
models.post.get({...})
models.post.update({...})

所以呢,我們只需要在 .jslib 文件里定一個萬能的 Models_API 方法,就全都搞定了

// Assets/Plugins/tcbsdk.jslib

Models_API: async function (callbackId, apiName, params) {
    callbackId = UTF8ToString(callbackId);
    apiName = UTF8ToString(apiName);
    const { modelName, options: optionsStr } = asmLibraryArg
      .Utils()
      .parseInputParams(params);
    const options = JSON.parse(optionsStr);
    const app = asmLibraryArg.Utils().getApp();
    
    const models = app.models;
    const { data } = await models[modelName][apiName](options);
    asmLibraryArg.Utils().sendMessage(callbackId, data);
},

同樣在 C# 腳本中實現(xiàn)一個萬能的 ModelAPI 方法,而 Get, Create 等方法其實就是通過 ModelAPI 方法來實現(xiàn)的

// Assets/Scripts/TCBSDK.cs

public class TCBSDK : MonoBehaviour
{        
    private class Models : IModels
    {
        private static readonly Lazy<Models> _instance = new Lazy<Models>(() => new());
        public static Models Instance => _instance.Value;
        private Models() { }

        public Task<T> Get<T>(ModelsReqParams input)
        {
            return ModelAPI<T>(input, "get");
        }
        public Task<T> Create<T>(ModelsReqParams input)
        {
            return ModelAPI<T>(input, "create");
        }   
        ...

        private async Task<T> ModelAPI<T>(ModelsReqParams input, string apiName)
        {
            var realInput = new Dictionary<string, object>
            {
                ["modelName"] = input.modelName,
                ["options"] = JsonConvert.SerializeObject(input.options)
            };

            (string, TaskCompletionSource<string>) asyncTask = Internal.GetAsyncTask();

            Internal.Models_API(asyncTask.Item1, apiName, Internal.ParseInputParams(realInput));

            string result = await asyncTask.Item2.Task;
            return Internal.ParseOutputResult<T>(result);
        }
    }

    private class Internal
    {
        [DllImport("__Internal")]
        public static extern void Models_API(string callbackId, string apiName, string input);
    }   
}   

public class ModelsReqParams
{
    public string modelName { get; set; }
    public Dictionary<string, object> options { get; set; }
}

丹尼爾:入?yún)?options 是個 Dictionary 類型,會不會不太好,是不是還是明確類型好點?

蛋先生:這個問題問得好!如果從代碼編寫規(guī)范來說,當(dāng)然是要定義類型的,裸奔畢竟不太好。但從實際使用角度來看,Dictionary 反而可能是個不錯的選擇。因為它省時省力,還對開發(fā)者相當(dāng)友好。

丹尼爾:對開發(fā)者友好,這怎么說?

蛋先生:你看看騰訊云開發(fā)的云后臺,在每個數(shù)據(jù)模型旁邊,都會貼心地給出常用 API 的代碼范例,基本上復(fù)制過來,稍微改改就能用。

10.png

所以,你可以在 web 環(huán)境下先盡情調(diào)試 JSON 入?yún)?,直到結(jié)果符合預(yù)期。然后再把這 JSON 扔給 GPT,讓它幫你生成對應(yīng)的 C# Dictionary,一氣呵成

11.png

最后,把代碼貼過來,大功告成~

var options = new Dictionary<string, object>
{
    ["filter"] = new Dictionary<string, object>
    {
        ["where"] = new Dictionary<string, object>
        {
            ["$and"] = new List<Dictionary<string, object>>
                {
                    new Dictionary<string, object>
                    {
                        ["_id"] = new Dictionary<string, string>
                        {
                            ["$eq"] = id
                        }
                    }
                }
        }
    }
};
ModelHello hello = await TCBSDK.Instance.ModelsGet<ModelHello>(new ModelsReqParams() { modelName = "hello", options = options });

丹尼爾:也對,真正的類型校驗,云開發(fā)的數(shù)據(jù)模型會嚴(yán)格把關(guān),SDK 只要開發(fā)者用得順手,問題不大

業(yè)務(wù)邏輯服務(wù)接口實現(xiàn)

丹尼爾:最后聊一下業(yè)務(wù)邏輯服務(wù)接口的實現(xiàn)吧,你覺得 Mock 數(shù)據(jù)要如何實現(xiàn)比較優(yōu)雅?

蛋先生:通過 interface 聲明接口,然后通過 UNITY_EDITOR 預(yù)處理符號來判斷應(yīng)該采用哪種實現(xiàn),編輯器環(huán)境用 MockGameAPI,其它則用 RealGameAPI

    public interface IGameAPI
    {
        Task<ModelsList<GameRealm>> GetRealmList();
        ...
    }

    public class GameAPI
    {
#if UNITY_EDITOR
        public static IGameAPI Instance = new MockGameAPI();
#else
        public static IGameAPI Instance = new RealGameAPI();
#endif
    }

    public class MockGameAPI : IGameAPI
    {
        public Task<ModelsList<GameRealm>> GetRealmList()
        {
            return Task.FromResult(new ModelsList<GameRealm>() { records = new List<GameRealm> { new() { _id = "mock_realm_id", name = "默認(rèn)" } }, total = 1 });
        }
        ...
    }

    public class RealGameAPI : IGameAPI
    {
        public Task<ModelsList<GameRealm>> GetRealmList()
        {

            var options = new Dictionary<string, object>
            {
                ...
            };

            return TCBSDK.Instance.ModelsList<ModelsList<GameRealm>>(new ModelsReqParams() { modelName = Constants.ModelName.GameRealm, options = options });
        }
    }

這樣在調(diào)用服務(wù)接口來實現(xiàn)游戲業(yè)務(wù)邏輯時就不用關(guān)心環(huán)境了

var tcbList = await GameAPI.Instance.GetRealmList();

丹尼爾:明白了,感謝蛋兄,我準(zhǔn)備大干一場了

蛋先生:加油,再見

以上完整代碼請移步到倉庫:https://github.com/daniel-dx/unity-cloudbase-demo
代碼有點粗糙,僅供參考,還望見諒!


寫在最后,別有用心

作為一個前端開發(fā)者,零 Unity 零 C# 基礎(chǔ),2 周時間從入門到“精通”( _( ?Д?)? 從沒見過如此厚顏無恥之人),交出這份作業(yè)對我來說還算滿意

但還是想通過這篇文章來拋磚引玉,指不定有哪位大神能提供其它意想不到的解決方案呢,您說,是吧!( ̄︶ ̄)↗

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容