设计一个简易的引导任务框架(2) | 4.23粉丝赠书

今天是4.23世界读书日,公众号向支持的小伙伴们送出下面3本技术图书(三选一)!

参与方式:

  1. 本文点赞留言,必须超过20字,以及你想要的图书名字参与活动

  2. 积赞最多的前3名读者,将会获得赠书,三选一

  3. 活动截止时间:2021-4-24 20:00 明天晚上8点

请获奖读者,通过公众号后台发送截图和您的快递联系方式领取赠书,24小时未来领取的视为放弃。

 设计一个简易的引导任务框架

前文导读

上一篇分析了如何定位节点,如何显示节点遮罩,以及节点事件的确认,原理和方法是有了但要将整个逻辑链条串连起来,还需要下一翻功夫。

编写了一个简单的引导任务框架,想仅通过 JSON 配置的方式,完成上述步骤、任务的执行,实现一个配置式、可编程的引导框架,期望的是让非程序人员经过简单的学习,也能实现引导内容的制作,我们先看一个任务配置案例:

module.exports = {
    name: '进入商店',
    debug: true,
    steps: [
        {
            desc: '点击主界面主页按钮',
            command: { cmd: 'locator', args: 'Home > main_btns > btn_home'},
            delayTime: 1,
        },

        {
            desc: '点击主界面设置按钮',
            command: { cmd: 'finger', args: 'Home > main_btns > btn_setting'},
        },
    ]
};

下面是按此配置执行的效果:

引导框架—串联异步引导步骤

前面讲过,一个引导步骤中节点定位函数 godGuide.find() 是通过回调函数异步返回目标节点,用户对目标节点的点击确定也是异步的,因此任务中的每一个 step 都是异步的,为了方便对流程的异步控制,在这里使用了 async 这个三方库,如果你不习惯也可以更换为你熟悉的异步编程方式。

首先,我们看看任务配置中的 steps 异步串行处理:

run() {
    //串行处理 steps 数组中的每一项目元素
    async.eachSeries(this._task.steps, (step, cb) => {
        //_processStep 是一个异步函数,通过回调通知 step 处理完毕
        this._processStep(step, cb);
    }, () => {
        //this._task.steps 中所有步骤处理完成,关闭引导
        this._task = null;
        //引导结束,隐藏遮罩、手指节点
        this._mask.node.active = false;
        if (this._finger) {
            this._finger.active = false;    
        }
    });
}

async.eachSeries 是将 steps 数组中的元素依次迭代处理,具体的步骤处理我们封装在 引导类的 this._processStep 成员函数中,当 steps 数组中所有步骤执行完毕,async.eachSeries 最后一个回调函数被触发,退出引导状态。

引导步骤—步骤生命周期回调与步骤指令

上面是控制的是引导整体流程,我们再深入到 this._processStep 函数:

_processStep(step, callback) {
     async.series({
         //步骤开始
         stepStart: (cb) => { 
             step.onStart ? step.onStart(this, cb) : cb();
         },
         
         //步骤指令
         stepCommand: (cb) =>  {
             this.scheduleOnce(() => {
                 this._processStepCommand(step, () => {
                        cb();
                    });
             }, step.delayTime || 0);  
         },
         
         //步骤结束
         stepEnd: (cb) => {
             step.onEnd ? step.onEnd.call(this, cb) : cb();
         }
     }, callback);
 },

1. 步骤生命周期回调

async.series 帮助我们串行执行多个异步函数,这里为 step 设计了 onStart、onEnd 两个生命周期回调,分别在上面 stepStart 和 stepEnd 中执行,我们可以在这两个函数中做一些初始化、条件检查等异步等待操作,例如:

  • 在 onStart 中等待玩家等级达到多少级,或某个事件发生;

  • 在 onEnd 中等待服务器返回某个消息、操作后等待某个动画的完成,可以通过监听事件进行确认。

下面代码是模拟道具购买的引导实现,可以具体了解到 onStart、onEnd 的使用方法

{
...
steps: [
        {
            desc: '10 级提示购买道具',
            //步骤开始
            onStart(callback) {
                let obj = {};
                //监听玩家等级变化
                cc.director.on('player-lv-up', (player) => {
                 //到达 10 级,显示商店界面
                 if (player.lv >= 10) {
                  cc.director.emit('show-shop');
                  //移除事件监听
                  cc.director.targetOff(obj);
                  //执行回调,执行步骤指令
                  callback();
                 }
                }, obj);
            },
            
            //步骤指令,定位商店界面中的购买按钮
            command: { cmd: 'finger', args: 'Shop > btnBuy'},
            
            //当玩家点击购买按钮,进入 onEnd 事件回调
            onEnd(callback) {
                //使用网络代理模块,监听指定网络事件
             NetProxy.once('message-buy-item', (msg) => {
                 //事件发生,执行 callback 回调步骤结束
                 callback();
             });
            }
        }
    ]
}

如果游戏比较简单 onStart 和 onEnd 不是必须的,通过 step 上 delayTime 属性可以做简单的延时控制,同样你也可以将游戏中增加事件、网络消息的广播编写成 step 配置中的 command 指令,以降低配置的复杂度。

2. 步骤指令

使用 step 指令,可以让步骤配置简化,特别是 UI 的点击引导。

步骤的异步处理过程中 this._processStepCommand 是关键,因为经常刚一进入某个场景时,可能需要定位的节点还未准备好(未创建或在动画运动过程中),我们又不想每个步骤都去写 onStart,因此步骤上提供了一个 delayTime 的属性,以延迟 step 指令的执行。

这里实现了两个指令:locatorfinger,他们的本质都是异步执行的函数 this._processStepCommand 中在对指令函数的调用,看下面代码:

let godCommand = require('GodCommand');
...
//处理步骤中的指令
_processStepCommand(cb) {
   let cmd = godCommand[step.command.cmd];
   if (cmd) {
       this.log(`执行步骤【${step.desc}】指令: ${step.command.cmd}`);
       cmd(this, step, () => { 
           this.log(`步骤【${step.desc}】指令: ${step.command.cmd} 执行完毕`);
           cb();
       });
   } else {
       cc.error(`执行步骤【${step.desc}】指令: ${step.command.cmd} 不存在!`);
   }
}

这里将指令函数编写在了名为 GodCommand.js 文件中,向指令函数传入当前引导对象step 配置对象,下面看定位指令的实现:

let GodCommand = {
    //定位节点
    locator(godGuide, step, callback) {
        //取出指令参数 
        let { args } = step.command;
        //调用引导类提供的定位节点
        godGuide.find(args, (node) => {
            //设置目标节点,用于遮罩显示和点击放行
            godGuide._targetNode = node;
            //点击确认
            node.once(cc.Node.EventType.TOUCH_END, () => {
                cc.log('节点被点击');
                //调用 callback 任务完成
                callback();
            });
        });
    },
}

可以看出这里又是一系列的回调:

  1. 从 step 中获取参数,调用 godGuide.find 定位节点;

  2. 目标节点定位成功,使用 node.once 注册临时触摸监听;

  3. 当目标节点触摸事件发生,执行 locator 输入的 callback 回调,指令完成。

需要注意,任务完成时一定要执行 callback,不然无法继续流程。有了该指令函数,就可以在任务配置文件中使用了,使用方式:

{
    desc: '点击主界面主页按钮',
    command: { cmd: 'locator', args: 'Home > main_btns > btn_home' },
    delayTime: 1,
}

step.command 中的 args 参数,由指令函数自行解释。

指令设计—实现手指动画指令

我们可以根据自己游戏的业务需求设计步骤指令,上一小节只是实现了节点的定位,并没有手指动画,在前面的基础上,我们为节点定位增加一个手指动画。

在 GodGuide 预制体上增加了一个手指预制体的属性,你可以根据自己的美术风格任意更换手指提示的表现,看下图:

手指预制体编辑界面:注意 GodFinger 预制体,锚点设置在了手指指尖位置。

手指动画提示可能比遮罩还常用,因此将手指动画的调用封装在了 GodGruid 组件代码中,提供了一个 fingerToNode 的函数,代码如下:

fingerToNode(node, cb) {
   // 手指节点不存在,直接回调
   if (!this._finger) {
       cb();
   }

   this._finger.active = true;
   // 转换节点位置
   let p = node.parent.convertToWorldSpaceAR(node.position)
   p = this.node.convertToNodeSpaceAR(p);
   
   // move 动作
   let duration = p.sub(this._finger.position).mag() / cc.winSize.height;
   let moveTo = cc.moveTo(duration, p);
   let callFunc = cc.callFunc(() => {
       cb(); // 完成回调
   })
   let sequnce = cc.sequence(moveTo, callFunc);
   this._finger.runAction(sequnce);
}

手指动画很简单,就是一个 moveTo 的动作,需要注意的是节点坐标转换和动作完成回调,下面是 finger 指令的实现:

let GodCommand = {
    //定位节点
    locator(godGuide, step, callback) {
        ...
    },

    //定位节点,显示一个手指动画
    finger(godGuide, step, callback) {
        let { args } = step.command;
        //定位节点
        godGuide.find(args, (node) => {
            godGuide._targetNode = node;
            //手指动画
            godGuide.fingerToNode(node, () => {
                //点击确认
                node.once(cc.Node.EventType.TOUCH_END, () => {
                    cc.log('节点被点击');
                    //任务完成
                    callback();
                });
            });
        });
    }
};

其实没什么东西,就在前面 locator 函数的基础上增加了 godGuide.fingerToNode 的调用。

指令设计—文本提示

在引导流程中,更为常规的做法是手指动画 + 提示文本,读者可以思考一下如何设计一个 text 的指令。

我们以测试驱动的方式,先给出引导配置:

{
...
steps: [
    {
     desc:'文本提示'
     command: { cmd: 'text', args: ['欢迎来到 xxx 游戏', 10001, 10002] };
    },
 {
     desc: '点击主界面主页按钮',
     command: { cmd: 'locator', args: 'Home > main_btns > btn_home'},
 },
]}

... ...

text 指令的参数是一个数组,可接受两种类型:

  1. 文本字符串:直接显示即可

  2. 语言配置 ID:有些游戏支持多国语言,在此直接配置语言 ID

同样,我们使用异步控制串行逐一输出 args 中的文本,当玩家点击屏幕时输出下一条文本,这里就不在帖出代码了。

小结

Step指令 都是可扩展、可编程的, 在实际的项目中,我们可能需要根据具体业务需求,设计出更多的指令,方便引导任务的配置,例如:ScrollView 列表滑动指令、节点关闭指令、玩家等级变化指令、玩家过关指令等等,指令的设计主要是对事件的监听和异步流程的控制 。

下一次,我们再给大家介绍,关于自动化的实现。

GodGuide 框架已经上架 Cocos Store ,而且最近开发者们又来上新了一大堆好玩、有趣、有价值的内容,点击【阅读原文】来看看吧!

4.23粉丝福利,快来参与吧!

参与方式:

  1. 本文点赞留言,必须超过20字,以及你想要的图书名字参与活动

  2. 积赞最多的前3名读者,将会获得赠书,三选一

  3. 活动截止时间:2021-4-24 20:00 明天晚上8点

请获奖读者,通过公众号后台发送截图和您的快递联系方式领取赠书,24小时未来领取的视为放弃。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值