1. 程式人生 > >Egret QQ玩一玩適配【踩坑日記】

Egret QQ玩一玩適配【踩坑日記】

需要申明一點,這是我接過最坑的渠道了,各種神奇的問題,首先是介面比較奇怪而且新舊版本搞得很混亂,其次是平臺底層實現效能差而且很多限制。此外,這裡需要理清楚一個概念:QQ 玩一玩QQ 玩吧 並非同一個東西,QQ 玩一玩也叫 QQ 輕遊戲釐米遊戲 ,是基於 bricks 引擎實現的。

技術限制

  • 玩一玩平臺不支援基於DOM Document物件的HTML元素處理

  • 玩一玩平臺不支援面板的遠端載入,所有面板必須宣告到egret專案的 default.thm.json 檔案當中。

  • 不允許動態執行程式碼能力。

不支援的功能:

渲染相關

  • 不規則遮罩

  • 動態截圖

  • 點陣圖快取

觸控相關

  • 畫素級碰撞檢測

  • 點選穿透

除錯相關

  • 髒矩形除錯顯示

  • fps監視器

  • 螢幕除錯日誌

適配步驟

參考 Egret 的官方文件 ,我們當前使用的引擎版本為 5.2.6 ,直接使用 egret run –target bricks 命令生成一個 Xcode 工程。

假如 iOS 系統是最新的 11.4.1 ,則需要安裝最新版本的 Xcode (這也要求 Mac 是 10.13.2 或更新的系統)

由於我們遊戲中使用了 fairygui 來製作 UI,在釐米遊戲這裡需要使用二進位制的匯出形式,原來的 xml 匯出無法加載出來。即全域性設定中:副檔名改為 zip ,然後勾選 使用二進位制格式

資源管理

玩一玩與微信在資源限制上是相似:

  • 微信要求首包 4M 以內,其他資源放在 CDN;

  • 手 Q 玩一玩要求首包 10M 以內,其他資源放在 CDN。

打包釋出

  • 打包:

    參考官方文件 檔案打包與執行 的相關規則,將程式碼和部分資源打成一個 zip 包,命名為 cmshow_game_xxx.zip ,其中 xxx 是遊戲的 gameId 。

  • 測試:

    從手 Q7.6.0 開始 QQ 輕遊戲不再提供特殊版本手 Q,開發者統一通過上傳指令碼至管理端進行測試。

我們遊戲使用的是 egret 引擎,在 Egret Launcher 中可以直接釋出 QQ玩一玩

的版本包,其釋出出來的是一個 Xcode 工程,而工程中 PublicBrickEngineGame\Res 目錄下便是最終上傳給平臺的遊戲程式碼包。除了首次釋出需要用到 Egret Launcher ,之後更新程式碼直接使用 egret publish --target bricks 即可。

常見報錯

2018-09-05 11:32:37.055825+0800 PublicBrickEngineGame[76458:6693859] [MC] Lazy loading NSBundle MobileCoreServices.framework
2018-09-05 11:32:37.056608+0800 PublicBrickEngineGame[76458:6693859] [MC] Loaded MobileCoreServices.framework
level=1, code=0, info1=SetKeepScreenOn , info2=, info3= 
level=0, code=0, info1=brick_log, info2=filemanage.js is loaded, info3= 
level=0, code=0, info1=brick_log, info2=Load Canvas.js succeed., info3= 
level=0, code=0, info1=brick_log, info2=Load Sprite.js succeed., info3= 
level=0, code=0, info1=BK.File.bkIsFileExist! error = No such file or directory, info2=, info3= 
level=1, code=0, info1=brick_log, info2=Xcode 環境, info3= 
level=0, code=0, info1=brick_log, info2=Load SandBoxCanvas.js succeed., info3= 
level=1, code=0, info1=brick_log, info2= Using default export (`import mobx from 'mobx'`) is deprecated and won’t work in [email protected]
Use `import * as mobx from 'mobx'` instead, info3= 
level=1, code=0, info1=brick_log, info2= [11:32:40+239ms] Failed getting local storage item (key: MusicEnabled)., info3= 
level=1, code=0, info1=brick_log, info2= [11:32:40+241ms] Failed getting local storage item (key: SoundEnabled)., info3= 
level=1, code=-1, info1=BK.TickerPlt.-[TickerPlatform onFrame:]! isAppActive = 0, info2=, info3= 

關鍵問題在於最後一句 BK.TickerPlt.-[TickerPlatform onFrame:]! isAppActive = 0 直接黑屏 QQ玩一玩XCode執行模擬器黑屏,求助 是 egret 的bug ,更新引擎只 5.2.8,然後在 js/main.min.js 末尾加入如下內容:

;global.Main = Main;

載入資源卡住:

info2=BK.MQQ.SsoRequest.callback errCode:1 

這是通過 Http 去載入圖片時出現的,這是表示索取的資源地址不存,檢測 CDN 資源即可。

WebSocket

這是需要使用 BK.WebSocket 進行重寫的部分,使用普通的 websocket 庫的話會在獲取 websocket 的 readyState 時報錯。

假如是斷開網路:

會先有 BK.Socket.DisconnectEvent 事件,然後觸發 onError 回撥,並帶有錯誤碼和錯誤資訊:

錯誤碼:1006
錯誤資訊:abnormal closure

息屏 > 5 分鐘,伺服器會主動斷開連線,也會在 websockt 的 onError 會收到錯誤碼:

錯誤碼:1006
錯誤資訊:abnormal closure

假如是伺服器通過直接斷開 TCP 實現主動將玩家踢下線的話,需要主動掉一次 close 再建立新的連線,否則會一直卡在 connecting 狀態沒辦法切換到 connected 狀態。

設計斷線重連方案:

  • 不做自動重連,只有傳送網路請求時才檢測當前網路的可用性並進行重連恢復

  • 當出現異常狀況只記錄網路不可用狀態

  • 假如網路正常但發包等待回包超時,需要彈窗提示

  • 被踢下線也應該有彈窗提示

  • 恢復網路前先檢測網路是否可用,假如不可用需要彈窗提示

  • 恢復失敗需要彈窗提示

另外,假如網路協議傳送底層實現是佇列式非同步傳送而非阻塞的話,最好把登入和重登這些與業務邏輯無關的協議使用單獨阻塞的通道去傳送,不要與業務佇列混到一起,否則處理重連時會很混亂。

資料轉化

一般的平臺都是使用 ArrayBuffer 來儲存二進位制資料,而釐米秀使用了自己的一個特殊的結構 BK.Buff ,所以需要兩個介面來實現:

  • ArrayBuffer 轉 BK.Buff

    // ArrayBuffer 轉為 BK.Buff
    public getBKBuffFromArrayBuffer(arrayBuffer: ArrayBuffer) {
        let len = arrayBuffer.byteLength;
        let bkBuff = new BK.Buffer(len, true);
        let uint = new Uint8Array(arrayBuffer);
        for (let i = 0; i < len; i++) {
            bkBuff[i] = bkBuff.writeUint8Buffer(uint[i]);
        }
        return bkBuff;
    }

  • BK.Buff 轉 ArrayBuffer

    // BK.Buff 轉為 ArrayBuffer
    public getArrayBufferFromBKBuff(bkBuff: BK.Buffer) {
        let len = bkBuff.bufferLength();
        let array = new Uint8Array(len);
    ​
        for (let i = 0; i < len; i++) {
            array[i] = bkBuff.readUint8Buffer();
        }
        return array.buffer;
    }

除錯

Xcode 除錯

使用 Egret Launcher 工具打出的包本身就是一個 Xcode 工程,可以在 Mac 下的 Xcode 開啟,然後安裝到 iOS 手機上運行遊戲。

除了可以直接在 Xcode 中看到執行的日誌,還可以直接在 Safair 上遠端除錯遊戲,具體步驟參考:Xcode工程下使用Safari遠端除錯遊戲 。上面的方法也只能解決一些類似資源載入和儲存、網路和音訊,但關於平臺登入相關的就測試不了,因為通過 openId 獲取 openKey 會返回錯誤碼 1;

VSCode 除錯外掛

為此,釐米遊戲官方也推出了替代的除錯方案,即藉助 Visual Studio Code 和額外的外掛,實現在 Windows 和 Mac 下實現除錯。參考 7.3 除錯工具 在 VS Code 中安裝對應的除錯外掛。

在 VS Code 中開啟 PublicBrickEngineGame\Res 目錄,然後開啟 main.js 指令碼(這是釐米遊戲的啟動入口),在編輯視窗的右上角會出現 應用管理除錯部署配置 4 個按鈕。根據提示配置相應的 gameId 、appId 和 appKey 即可開始除錯。

然而,使用 node.js 版本出現 使用檢查器協議進行除錯,因為無法確定 Node.js 版本 (Error: connect ECONNREFUSED 127.0.0.1:2507) 錯誤,看了 pre-attach.js 原始碼發現是呼叫 adb reverse 的時候報錯,那麼問題就出在 adb 工具,結果發現原本使用的 adb 工具的版本為 1.0.31 ,換成 1.0.39 的版本就正常了。

而使用 python 版本則打開了 debugBrick.apk ,而且按照外掛中的 pre-attach.py 這個任務 python 指令碼執行到最後的 try to attach brick ... 日誌,但是啟動便黑屏了,也沒有任何日誌輸出。

常見問題

1.賬號許可權的問題

由於除錯工具黑屏無法顯示介面內容,所以只能選擇上傳到後臺再掃碼測試了,結果出現了:

使用手機掃碼之後,提示 該遊戲已下架,無法繼續玩耍 ,原因是管理後臺開發管理處提示: Warning: appid沒有通過稽核,不可申請上架;

2.啟動報錯

官方客服提示只能用 Android 手機進行測試,掃碼提示 啟動失敗,請稍後重試哦~ ,然後在手機檔案管理器中開啟 內部儲存/tencent/MobileQQ/.apollo/game 目錄下(是隱藏目錄,需要設定顯示隱藏檔案或資料夾才能檢視),檢視是否有以遊戲 GameId 取名的目錄和 .zip 包,這就是遊戲包體儲存的資料夾。

平臺包內檔案的快取路徑:GameRes://resource/GameSandBox:// 實際上也都在 game/xxx 目錄下。

最後,發現使用開發者主賬號一直啟動不了,而使用另一個 QQ 新增到白名單反而可以,十分奇葩的問題。

3.adb 檢視報錯日誌

確保 adb devices -l 下可以看到手機裝置,然後使用 adb logcat 來捕獲遊戲日誌,需要使用 sava_native_log 標籤來進行過濾:

adb shell "logcat |grep sava_native_log"

其中 sava_native_log 是玩一玩日誌輸出的標識。或者,直接寫入到檔案中:

adb logcat -v time process > log.txt

需要注意,使用 BK.Script.log 列印的日誌在這裡是不會輸出的,只會輸出 console.log 的日誌。

4.Android 掃碼啟動卡在 99%

通常是由於程式碼報錯導致的,經過一下午的排查才發現是少了 promise.js 的引入(參考:玩一玩遊戲FAQ

5.動畫很鬼畜或抖動,遮罩不生效

這是因為打包的時候,預設幀率是 30 :

egret.runEgret(
    {
        renderMode: window.renderMode,
        frameRate: 30,
        contentWidth: 640,
        contentHeight: 1136,
        entryClassName: "Main",
        scaleMode: "showAll",
        orientation: "auto",
        background: 0x888888
    }
);

應該改為 60 幀,然後 UI 適配方案根據自己遊戲的情況調整:

egret.runEgret(
    {
        renderMode: window.renderMode,
        frameRate: 60,              // 預設是 30 幀,會有各種鬼畜的問題(動畫抽搐、遮罩變黑)
        contentWidth: 720,          // 預設是 640 X 1136
        contentHeight: 1280,
        entryClassName: "Main",
        scaleMode: "fixedWidth",    // 預設 showAll
        orientation: "portrait",    // 預設 auto
        background: 0x888888
    }
);

6.掃碼彈窗提示報錯

在掃碼之後彈窗報錯資訊如下:

[game:3958]Execute JS Error!
[TypeError:undefined is not an object (evaluating 'e.prototype')]:line = 13,column = 20,

可以直接將 內部儲存/tencent/MobileQQ/.apollo/game/遊戲GameId/main.js 指令碼複製到打包前的工程中,替換原本的 main.js ,使用 VS Code 除錯工具除錯,可以看到執行輸出:

*********************************出現未捕獲異常***********************************
11-01 10:38:40.875 17189 17213 I System.out: sava_native_log [printNativeLog], level:1,code:1,info1:jswrapper.__onMessageCallback! brick_exception ERROR: Uncaught TypeError: Cannot read property 'prototype' of undefined, /sdcard/tencent/MobileQQ/Test/main.js:0:0
**********************************異常資訊結束***********************************
*********************************出現未捕獲異常***********************************
11-01 10:38:40.875 17189 17213 I System.out: brick_exception STACK:
11-01 10:38:40.875 17189 17213 I System.out: brick_exception [0][email protected]/sdcard/tencent/MobileQQ/Test/main.js:13
**********************************異常資訊結束***********************************

定位到問題出現在 main.js 中的第 13 行,即 r.prototype = e.prototype, t.prototype = new r(); 這一行,那麼加一個 try catch 捕獲導致異常的呼叫:

var window = this;
var global = global || this;
global.bricks = {};
this.navigator = { userAgent: 'bricks' };
this.setTimeout = this.setTimeout || function () {
};
var __extends = function (t, e) {
    function r() {
        this.constructor = t;
    }
    for (var i in e)
        e.hasOwnProperty(i) && (t[i] = e[i]);
    try{
        r.prototype = e.prototype, t.prototype = new r();
    }catch(e){
        console.log('err = '+t.name);
        console.log('err = '+e.stack);
    }
};

再次除錯,發現報錯呼叫堆疊資訊:

[11-1-2018 11:19:19]  [BRICK_LOG] LEVEL = 1, ERRCODE = 0, INFO =  err = AdapterTexture
[11-1-2018 11:19:19]  [BRICK_LOG] LEVEL = 1, ERRCODE = 0, INFO =  err = TypeError: Cannot read property 'prototype' of undefined
    at __extends (/sdcard/tencent/MobileQQ/Test/main.js:14:20)
    at /sdcard/tencent/MobileQQ/Test/main.js:66122:9
    at window.spine.window.spine (/sdcard/tencent/MobileQQ/Test/main.js:66138:6)
    at /sdcard/tencent/MobileQQ/Test/main.js:66267:2
    at /sdcard/tencent/MobileQQ/Test/main.js:158576:2

我出現此問題的原因是:修改了 spine 執行時的引入方式,之前的做法是原始碼引入跟工程一起打包,後來把它單獨抽出來,以庫的方式引入,從而導致了此問題。

7.遊戲內所有文字往下偏移

這是因為 egret.brick.js 在將 TextField 轉為 BKTextField 時計算高度有問題,修改如下:

// 修改前
BKCanvasRenderer.prototype.renderText = function (node, context) {
    ...
    context.fillText(text, x + context.$offsetX, -y + context.$offsetY + node.height);
}
// 修改後
BKCanvasRenderer.prototype.renderText = function (node, context) {
    ...
    context.fillText(text, x + context.$offsetX, -y + context.$offsetY + node.height + context.lineWidth + 2);  // 解決文字整體下移的問題
}

8.邀請類分享

分享可用的介面有兩個:

  • share

  • shareToArk

給分享的 extendInfo 塞入擴充套件資訊,然後在 onLoad 和 onEnterforeground 監聽中去通過 GameStatusInfo.gameParam 獲取這個擴充套件資訊。

9.玩家頭像

與其他渠道不同,玩一玩獲取玩家頭像的方式並非在登入渠道時直接返回頭像的網路地址,而是需要使用玩家的 openId 通過 BK.MQQ.Account.getHeadEx 介面去下載頭像,下載頭像儲存的路徑是:"GameSandBox://_head/" + openId + ".jpg" ,大概邏輯如下:

let openId = GameStatusInfo.openId;
let tex = await new Promise<egret.Texture>(resolve => {
    let absolutePath = "GameSandBox://_head/" + openId + ".jpg";
    let isExit = BK.FileUtil.isFileExist(absolutePath);
    gLog(absolutePath + " is exit :" + isExit);
    //如果指定目錄中存在此影象就直接顯示否則從網路獲取
    if (isExit) {
        loadTexture(absolutePath).then(tex => {
          resolve(tex);
        });
    } else {
        BK.MQQ.Account.getHeadEx(GameStatusInfo.openId, function (oId, imgPath) {
          gLog("openId:" + oId + " imgPath:" + imgPath);
          loadTexture(imgPath).then(tex => {
            resolve(tex);
          });
        });
    }
})
return tex;
​
function loadTexture(path: string){
    let texture = new egret.Texture();
    let imageData = new egret.BitmapData(path);
    gLog('圖片:' + path + '載入成功');
    texture._setBitmapData(imageData);
    return texture;
}

其次,圓形頭像裁剪似乎有問題,大概情況是:

  • 使用 Shape 作為 mask 的或直接導致裁剪後的圖片不顯示;

  • 使用不透明的圓形圖片作為 mask 的話,有些裝置上能裁剪成功,但有些裝置上也會出現圖片直接顯示不出來的情況。

因此,玩一玩平臺中使用玩家頭像的成本比較高:一方面是不支援額外的裁剪,只能使用方形的;另一方面是需要先下載頭像到本地才能展示。

效能測試

玩一玩提交稽核時需要提供自測報告,可以藉助 wetest 工具來測試,Android 手機下載 WeTest 助手 ,然後在助手中註冊一個賬號,選擇 通用效能分析 ,選擇 QQ 應用,然後點選 開始測試 然後在 QQ 中開啟遊戲開始玩遊戲即可。(前提是手機已經 root 了,假如是非 root 的手機,則還需要藉助 PC 上的 cube-pc 助手),手機用 USB 線連著 pc 助手進行測試。但是,非 root 的手機無法測試 Mono 記憶體和 Drawcall)。

此外需要在設定中開啟彈出懸浮框的許可權

測試報告需要在網頁開啟 通用測試報告 ,登入與助手一致的 WeTest 賬號,即可檢視測試資料。

效能瓶頸

  • FPS (幀率)

    問題:首先是 FPS 幀率方面,在 QQ 玩一玩平臺,使用 egret 引擎開發的遊戲中假如使用 spine 動畫會有明顯的掉幀現象。

    解決方案:將所有的 spine 動畫通過 DragonBones Pro 轉為二進位制格式的龍骨動畫,掉幀的問題基本上能夠解決。

  • 記憶體和 DrawCall

    在 WeTest 上想要測這兩項資料需要 root 手機才能獲得

參考