今天是4.23世界读书日,公众号向支持的小伙伴们送出下面3本技术图书(三选一)!
参与方式:
本文点赞留言,必须超过20字,以及你想要的图书名字参与活动
积赞最多的前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 指令的执行。
这里实现了两个指令:locator和finger,他们的本质都是异步执行的函数 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();
});
});
},
}
可以看出这里又是一系列的回调:
从 step 中获取参数,调用 godGuide.find 定位节点;
目标节点定位成功,使用 node.once 注册临时触摸监听;
当目标节点触摸事件发生,执行 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 指令的参数是一个数组,可接受两种类型:
文本字符串:直接显示即可
语言配置 ID:有些游戏支持多国语言,在此直接配置语言 ID
同样,我们使用异步控制串行逐一输出 args 中的文本,当玩家点击屏幕时输出下一条文本,这里就不在帖出代码了。
小结
Step和指令 都是可扩展、可编程的, 在实际的项目中,我们可能需要根据具体业务需求,设计出更多的指令,方便引导任务的配置,例如:ScrollView 列表滑动指令、节点关闭指令、玩家等级变化指令、玩家过关指令等等,指令的设计主要是对事件的监听和异步流程的控制 。
下一次,我们再给大家介绍,关于自动化的实现。
GodGuide 框架已经上架 Cocos Store ,而且最近开发者们又来上新了一大堆好玩、有趣、有价值的内容,点击【阅读原文】来看看吧!
4.23粉丝福利,快来参与吧!
参与方式:
本文点赞留言,必须超过20字,以及你想要的图书名字参与活动
积赞最多的前3名读者,将会获得赠书,三选一
活动截止时间:2021-4-24 20:00 明天晚上8点
请获奖读者,通过公众号后台发送截图和您的快递联系方式领取赠书,24小时未来领取的视为放弃。