Cocos Creator开发中的记录

919 阅读6分钟

写在前面

用这个记录一下在使用cocos creator中的坑和解决问题的办法,一来以后再遇到此类问题有迹可循,二来方便以后面试之前看看

问题

ScrollView/ListView在节点item较多的情况下,drawcall飙升,造成严重的耗电和卡顿

解决方案

1.复用节点item

复用节点是解决ScrollView/ListView卡顿的根本方法;具体实现可以参考官方的ListView例子

    update: function(dt) {
        this.updateTimer += dt;
        if (this.updateTimer < this.updateInterval) return; // we don't need to do the math every frame
        this.updateTimer = 0;
        let items = this.items;
        let buffer = this.bufferZone;
        let isDown = this.scrollView.content.y < this.lastContentPosY; // scrolling direction
        let offset = (this.itemTemplate.height + this.spacing) * items.length;
        for (let i = 0; i < items.length; ++i) {
            //获取item相对view的坐标
            let viewPos = this.getPositionInView(items[i]);
            //如果Item已经超出view,用新的itemid对应的数据,刷新该item的显示
            //最好在item中实现一个自刷新的方法,在收到超出view的通知后自动刷新显示
            if (isDown) {
                // if away from buffer zone and not reaching top of content
                if (viewPos.y < -buffer && items[i].y + offset < 0) {
                    items[i].y = items[i].y + offset;
                    let item = items[i].getComponent('Item');
                    let itemId = item.itemID - items.length; // update item id
                    item.updateItem(itemId);
                }
            } else {
                // if away from buffer zone and not reaching bottom of content
                if (viewPos.y > buffer && items[i].y - offset > -this.content.height) {
                    items[i].y = items[i].y - offset;
                    let item = items[i].getComponent('Item');
                    let itemId = item.itemID + items.length;
                    item.updateItem(itemId);
                }
            }
        }
        // update lastContentPosY
        this.lastContentPosY = this.scrollView.content.y;
        this.lblTotalItems.textKey = "Total Items: " + this.totalCount;
    },

2.控制节点摆放顺序,尽量避免将sprite和label穿插,以免打断合批

降低drawcall的有效手段就是合批,将很多贴图合批一次渲染,但label并不能合批,所以当sprite和label穿插时,就会打断合批,从而导致drawcall增加(即使所有的贴图都是来自一张大图),还有cocos提供的mask,也会打断合批,在使用mask的时候需要格外注意,例如mask实现圆形头像遮罩等,可以通过其他方法折中实现

错误例子:

正确实现:

当然在开发的过程中,肯定会有无法避免的节点层级问题,就仁者见仁了

问题

cocos creator中事件传递提供了node.on和node.emit,但是只能在同一个节点上传递,如果想跨节点甚至不依赖UI节点去传递事件,就比较困难了

解决方案

cc.game.on和cc.game.emit,哈哈哈,简单粗暴,但是好像性能消耗比较大,可以自己在cc或window上,声明一个全局变量,通过此全局变量来传递事件

//GlobEventProcess.js
(function () {
    var list = {};
    var __EventMgr = {
        //注册消息
        on: function (key, func, node) {
            if (list[key] == null) {
                list[key] = [];
            }
            var d = {};
            d.func = func;
            d.node = node;
            list[key].push(d);
        },
        //移除一个消息
        remove: function (key, func, node) {
            if (list[key]) {
                var data = list[key];
                for (var i = 0; i < data.length; i++) {
                    if (data[i].node == node && data[i].func.name == func.name) {
                        data.splice(i, 1);
                        --i;
                    }
                }
            }
            // list[key].push(func);
        },
        //移除node所有消息
        removeByNode: function (node) {
            for (var p in list) {
                var data = list[p];
                for (var i = 0; i < data.length; i++) {
                    if (data[i].node == node) {
                        data.splice(i, 1);
                        --i;
                    }
                }
            }
        },
        //移除key注册的所有消息
        removeByKey: function (key) {
            delete list[key];
        },
        //遍历通知消息
        emit: function (key, arg) {
            if (list[key]) {
                var funcs = list[key].concat();
                for (var i = 0; i < funcs.length; i++) {
                    if ((funcs[i].node instanceof cc.Component)) {
                        if (cc.isValid(funcs[i].node)) {
                            funcs[i].func(arg, funcs[i].node);

                        }
                    }
                    else {
                        funcs[i].func(arg, funcs[i].node);

                    }
                }
            }
        }
    };
    window.EventMgr = __EventMgr;
})();

问题

js的this指针问题,在开发过程中,经常会遇到此this非彼this的情况,刚开始着手时很懵,无从下手。(还是js基础差的原因)

解决方案

call和apply是js常用的绑定方法,可以自行学习,我在开发的过程中,主要是跨脚本传回调的时候用的比较多,此类解决方法我一般统一在回调后.bind(this),妈妈再也不用担心我的this找不到正确的路了

//事件接收组件
reciver.js
cc.Class({
    extends: cc.Component,

    properties: {
        avatar: cc.Node,
        registerBtn: cc.Button,
        loginBtn: cc.Button,
        loginPrefab: cc.Prefab

    },

    onLoad: function () {
        //此处EventMgr用上一个问题的全局事件管理类
        EventMgr.on('login', this.refreshUI.bind(this), this);
    },

    refreshUI() {
        this.avatar.active = true;
        this.registerBtn.node.active = false;
        this.loginBtn.node.active = false;
    }
});
//事件发送组件
send.js
cc.Class({
    extends: cc.Component,

    properties: {
        loginBtn: cc.Button,
    },

    onLoad: function () {
        this.loginBtn.node.on('click', () => {
            //此处EventMgr用上一个问题的全局事件管理类
            EventMgr.emit('login');
            this.node.destroy();
        })
    },
});

或者用ES6的箭头函数

//事件接收组件
reciver.js
cc.Class({
    extends: cc.Component,

    properties: {
        avatar: cc.Node,
        registerBtn: cc.Button,
        loginBtn: cc.Button,
        loginPrefab: cc.Prefab

    },

    onLoad: function () {
        //此处EventMgr用上一个问题的全局事件管理类
        EventMgr.on('login', ()=>{
            this.avatar.active = true;
            this.registerBtn.node.active = false;
            this.loginBtn.node.active = false;
        }, this);
    },
});
//事件发送组件
send.js
cc.Class({
    extends: cc.Component,

    properties: {
        loginBtn: cc.Button,
    },

    onLoad: function () {
        this.loginBtn.node.on('click', () => {
             //此处EventMgr用上一个问题的全局事件管理类
            EventMgr.emit('login');
            this.node.destroy();
        })
    },
});

问题

cc.runAction时,一个action的target只能有一个

解决方案

如果想让多个节点执行同一个动作,可以用action.clone();

let action = cc.moveTo(1,0,100);
node1.runAction(action);
node2.runAction(action.clone());

问题

安卓返回键监听

解决问题

相信很多游戏可能都有这个需求,什么弹窗打开用返回键关闭,退出游戏巴拉巴拉,反正有这个需求就是了; 先看官方文档:docs.cocos.com/creator/api…, 查找自己想用的keyCode

    //响应事件
    onKeyDown(event) {
        switch (event.keyCode) {
            case cc.KEY.back:
                //处理返回键逻辑
                this.processBack();
                break;
            default:
                break;
        }
    },

    //添加事件,一般在节点初始化时,切记一个节点只添加一次
    addSysEvent() {
        //只有安卓有,在此判断一下os类型
        if (cc.sys.os == cc.sys.OS_ANDROID) {
            cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
        }
    },
    
    //移除事件,注意在节点隐藏或销毁后,一定要移除此事件,因为是系统级别的事件监听,返回键按下后会通知所有的监听回调
    removeSysEvent() {
         //只有安卓有,在此判断一下os类型
        if (cc.sys.os == cc.sys.OS_ANDROID) {
            cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
        }
    },

问题

屏幕适配,移动端开发绕不过去的问题

解决方案

首先明确几个点:

1.设计分辨率

图中Design Resolution即为设计分辨率,也是美术设计的时候的资源分辨率,在做屏幕适配时,就是基于这个设计分辨率做的

2.适配策略

cocos creator提供的适配策略有四种,Fit Height(按高适配),Fit Width(按宽适配),和 Show All(同时勾选fit height和fit width,等比缩放显示整个场景),No Border(fit height和fit width都不勾选,这个适配模式是根据屏幕比例等比拉伸画面,

  • 有两种结果:
    • 当高度拉满,宽度超出屏幕的时候,裁切宽度。
    • 当宽度拉满,高度超出屏幕的时候,裁切高度。

)这部分官方文档:docs.cocos.com/creator/man… 说的很清楚了,我就不再重复了,理解消化一下;

3.对齐策略

官方说的很清楚:docs.cocos.com/creator/man…

那根据上面3点,选择自己对应的解决方案就很容易了,举一个小例子说明:

在这个场景中,头像,状态(时间,电量,网络),菜单都在靠近屏幕边缘的位置,如果不借用widget,就会出现部分超出或者离屏幕边缘较远的,不符合我们预期的表现;

所以为了解决这些问题,借用widget,让其无论屏幕大小缩放到多少,都停靠在屏幕边缘;