用Cocos2d-JS制作游戏新手引导教程1
在游戏开发中,新手引导的实现往往让开发者感到棘手,尤其是在游戏功能和需求不稳定的情况下。笔者就曾在这样的环境中承担了新手引导功能的开发任务。在开发过程中,我遇到了一些有趣的问题和解决方案,现在将其分享给大家。
一、痛点:新手引导制作的难点及弊端
- 干扰正常流程:需要在具有引导功能的代码单元插入引导代码或逻辑判断,这会干扰游戏的正常流程。
- 增加代码复杂度:引导代码的加入会影响原有的代码逻辑与流程,使代码变得复杂,加大维护难度。
- 修改成本高:界面或需求发生变化后,引导功能需要大幅修改或重新制作。
- 定位困难:指引(手指提示)对应的矩形区定位麻烦,特别是需要适应不同尺寸屏幕的时候更加困难。
- 协作成本高:编写引导配置文件需要策划、程序的高度配合,这增加了开发的难度和成本。
二、期望:新手引导编程体验
笔者从事手机游戏开发的时间不长,虽然参与过多个项目,但亲自编写新手引导还是第一次。当时项目只完成了登录 -> 主界面 -> 抽卡 -> 布阵 -> 章节 -> 关卡 -> 战斗这样一个基本流程,界面美术、功能需求都极不稳定。在公司的要求下,我开始了新手引导功能的开发。在了解到传统引导制作的难点后,我期望的引导编程方式如下:
- 代码分离:不需要在每个单元中插入引导代码,游戏代码与引导代码应尽量分离,避免漂亮的代码被引导逻辑打乱。
- 适应性强:界面只发生简单 UI 位移、节点层次改变时,不需要修改引导代码。
- 定位简单:定位指引矩形区应尽量简单,且能自适应不同尺寸屏幕,最好策划人员也能制作部分流程引导。
- 制作快捷:在引导需求明确、游戏功能正常的情况下,制作一个常规的引导步骤应非常快捷,不超过 3 分钟,快的话 1 分钟内即可完成(笔者已实现)。
三、思想:引导功能的设计思路
在描述引导功能的设计思路之前,有一个重要的前提:命名规范。命名规范主要涉及两个方面:
- Cocos Studio 中的控件名字。
- 代码中动态创建的控件名字,以及类成员变量的名字。
在笔者的项目中,使用了 sw.UILoader 来管理 Cocos Studio 的 UI 命名和事件。
任务与任务组的概念
- 任务:把引导中的一个最小步骤称之为一个任务,例如提示点击某个按钮。
- 任务组:把一系列的任务放在一个任务组中,当任务组中的任务全部完成,会保存一次任务进度。此时重新进入游戏将不再执行该任务组,而是执行下一个任务组中的任务。可以将任务组理解为引导中的一个步骤。
用 JSON 格式表示如下:
[
[{"任务1"}, {"任务2"}, {"任务3"}],
[{"任务7"}, {"任务8"}, {"任务9"}],
[{"任务4"}, {"任务5"}, {"任务6"}]
]
当从一个任务组中的任务中断后,再次进入引导需要重新从该任务组的第一个任务开始。
例如,从主界面点击召唤 -> 灵石召唤一次 -> 点击获得 -> 确定 -> 仙玉召唤一次 -> 点击获得 -> 确定 -> 点击空白退出召唤界面的流程,可分成两个任务组:灵石召唤、仙玉召唤。任务配置如下:
{
"3": [
{
"name": "4.提示指向灵石召唤按钮",
"command": "手型提示",
"tag": "_oneMoneyButton"
},
{
"name": "保存进度",
"command": "保存进度"
},
{
"name": "5.提示指向角色确定按钮",
"command": "手型提示",
"tag": "_UILotteryHero > _confirmBtn"
},
{
"name": "6.提示指向角色图标确定按钮",
"command": "手型提示",
"tag": "_UILotteryTimes > _confirmBtn"
}
],
"4": [
{
"name": "7.提示指向仙玉召唤按钮",
"command": "手型提示",
"tag": "_oneGoldButton"
},
{
"name": "保存进度",
"command": "保存进度"
},
{
"name": "8.提示指向角色确定按钮",
"command": "手型提示",
"tag": "_UILotteryHero/Panel_33/Image_10/_confirmBtn"
},
{
"name": "9.提示指向角色图标确定按钮",
"command": "手型提示",
"tag": "_UILotteryTimes/Panel_11/Image_1/_confirmBtn"
}
]
}
每个任务中的 name 用于调试打印,对引导本身无实际用处,在任务开始和结束时会有提示,方便出错时定位。command 可称为指令,对应一段具体功能的代码或函数,这里设置了两个:手型提示、保存进度。
- 手型提示:需要配合
tag字段的值,tag描述了当前任务状态下一个node节点的索引。具体tag的编写方式见下一节“实现在节点树中定位控件”。 - 进度保存:手动进度保存是为了确保在任务中断后,游戏流程不受影响。在召唤功能中,只能召唤一次,召唤成功后服务器已更新数据,后续引导为客户端的界面显示和关闭引导。在召唤后进行进度保存,任务中断后再次进入引导会跳过该任务组中的任务。
引导框架
在任务条件满足时(如等级达到一定要求或无任何条件),引导框架指示用户进行某项任务(如按钮点击)。当任务完成后,执行下一个任务,直到全部任务完成。它需要具备以下功能:
- 条件检查:检查是否该执行该任务,默认无条件执行。需要检查任务是否有
onTaskBegan函数,不存在或返回true才能执行任务指令。 - UI 定位:找到当前任务中 UI 节点对应的矩形区。在指引任务中准确编写 UI 定位描述,由框架检索 UI 节点,检索到节点后调用任务的
onLocateNode函数,传入节点对象,以便扩展引导功能。 - 指引动画:定位成功后,引导框播放指引提示动画,提示用户操作该矩形区。
- 触摸限制:屏蔽定位节点矩形区外的所有操作。
- 事件检查:检查矩形区对应的 UI 事件是否被执行。
- 任务完成:通知引导框架任务完成,进入下一个任务。
四、定位:实现在节点树中定位控件
要实现引导功能,首要解决的是对 UI 控件的定位。最直接有效的方法是获取 UI 控件对象,然后取出其 BoundingBox、锚点信息,进行坐标转换。获取控件对象有两种实现方式:
- 遍历场景树:通过遍历场景树将控件搜索出来。
- 注册控件对象:事先把控件对象注册到引导框架中。
笔者采用了第一种方法,因为不想在代码中添加游戏逻辑以外的东西。在 Cocos2d-JS 中,cc.helper.seekWidgetByName 函数可用于定位控件,但在手机游戏开发中不能直接使用。在 HTML5 上该函数可遍历整个节点树,在 JSB 上仅遍历 Widget 节点。解决这个问题有两种方法:
- 将
cc.helper.seekWidgetByName函数复制到自己的代码文件中,重新命名为xxx.helper.seekNodeByName,在 HTML5 和 JSB 上都使用该函数。 - 在 C++ JSB 上修改
cc.Helper.seekWidgetByName的参数,使其在Node节点上做遍历,或重新封装一个 JSB 上的seekNodeByName函数。
笔者选择了第一种方法。但通过这种方法定位控件也存在问题。如果一个场景树中有两个相同节点名字,使用 seekNodeByName 只能定位到其中一个,具体定位到哪个取决于 addChild 时的顺序。为了解决这个问题,可以使用节点的“完整路径”来唯一确定一个控件。例如,如果有两个名为 button 的节点,可以用“招唤界面/灵石招唤/召唤一次”和“招唤界面/仙玉招唤/召唤一次”来定位,也可以简化为“灵石招唤/召唤一次”和“仙玉招唤/召唤一次”。
定位器描述规则
为了方便在任务中定位控件,笔者实现了一个简易的定位器描述规则:
- 名字描述:当节点在场景中有独一无二的名字时,直接描述控件名,如
_loginButton。 - 路径名描述:当需要定位的节点可能有重名时,找到其父节点,确保父节点无重名,使用
parentName/button的形式。若父节点也有重名,则向上使用其父节点名的父节点,以此类推。 - js 属性描述:对于通过
getChildByName无法直接访问的节点,如ccui.ScollView容器中的节点,可使用layer1.button的形式,通过“.”符号定位layer1下属性为button的节点。 - 子节点描述:使用完整路径描述控件有时会较长,可简写成
mainLayer>button,表示定位mainLayer下一个名为button的子节点,可能是 1 级子节点,也可能是 2、3、n 级子节点。 - 复合描述:将以上几种方式组合使用,如
mainLayer>homeLayer/layer1.button,表示mainLayer下有一个homeLayer子节点(不管是几级),其一级子节点layer1下一个变量名为button的节点。
描述符号总结
- “/”:表示一级子节点。
- “>”:表示一级 ~ n 级子节点。
- “.”:表示属性名。
这里借鉴了 CSS 选择器的思想,实现了一个简单的选择器,可称之为“定位器”,因为我们只需要定位出一个节点。