想到新手引导的功能时可能很多人都会觉得头痛,难以下手。特别是在游戏本身功能或需求还不稳定的情况,更是难以应付,本人就是在这种情况下接受了一个艰巨的任务。在痛定思痛之后,开始了引导功能开发。在做的过程中一点点发现很多有意思的东西,想分享给大家。
一、痛点:新手引导制作的难点及弊端
二、期望:新手引导编程体验
笔者进入游戏开发应该说是手机游戏开发并不是很长时间,虽然参于过多个项目,但亲自编写新手引导这还是头一次。当时接到新人引导任务时,我们的项目只完成了:登录->主界面->抽卡->布阵->章节->关卡->战斗这样一个基本流程,界面美术、功能需求都极不稳定。但在公司的硬性要求下,冒着九死一生的危险开始了新手引导功能开发。在了解到传统的引导制作过程中的难点与弊端后,一直在思考没有更好的实现方式,我心中的引导编程的方式有以下几点:
-
不需要在每个单元中去插入引导代码,游戏代码与引导代码应该尽量分离。本人很难忍受漂亮的代码被无情引导打乱,更难忍受本来糟糕的代码被引导弄得支离破碎。
-
定位指引矩形区应该尽量的简单,且自适应不同尺寸屏幕。最好能做到策划人员都可以来制作部分流程引导。
-
在引导需求明确、游戏功能正常的情况下,制作一个常规的引导步骤应该是非常快捷的,不会超过3分钟,快的话1分钟内就应该搞定(不是笔者说大话,确实已经实现)。
三、思想:引导功能的设计思路
在描述引导功有设计思路之前,有个重要的前题:命名规范。
命名规范主要有两个方面:
在笔者的项目中使用了sz.UILoader来管理cocostudio的UI命名和事件。如不了解请参见我的另外一篇blog
《在cocos2d-js实现自动绑定cocostudioUI控件与事件》
我们这里引入两个概念:任务与任务组。任务:把引导中的一个最小步骤称之为一个任务,比如提示点击某个按钮。任务组:把一系列的任务放在一个任务组中,当这个任务组中的任务全部完成,我们会保存一次任务进度。此时重新进入游戏将不会再执行这个任务,而是执行它的下一个任务组中的任务。可以理解任务组是引导中的一个步骤。
用json格式表示如:
{
1: [{任务1},{任务2},{任务3}]
3: [{任务7},{任务8},{任务9}]
2: [{任务4},{任务5},{任务6}]
}
当从一个任务组中的任务中断后,再次进入引导 需要重新从这个任务组的第一个任务开始。
见下图演示了一个从主界面点击召唤->灵石召唤一次->点击获得->确定->仙玉召唤一次->点击获得->确定->点击空白退出召唤界面的流程。
上图演示的引导我分成两个任务组:灵石召唤、仙玉召唤。 任务配置如下:
"3":[
{
"name": "4.提示指向灵石召唤按钮","command": "手型提示""tag": "_oneMoneyButton" }"保存进度""保存进度" }"5.提示指向角色确定按钮""_UILotteryHero > _confirmBtn" }"6.提示指向角色图标确定按钮""_UILotteryTimes > _confirmBtn" }
]"4":[
{
"7.提示指向仙玉召唤按钮""_oneGoldButton" }"8.提示指向角色确定按钮""_UILotteryHero/Panel_33/Image_10/_confirmBtn" }"9.提示指向角色图标确定按钮""_UILotteryTimes/Panel_11/Image_1/_confirmBtn" }
其中每个任务中的name用于调试打印的对引导本身无实际用处,在任务开始和结速都会有提示,如果出错方便定位。 command这里应该叫做指令,对应一段具体功能的代码或函数,我这里设置了两个:手型提示、保存进度。
手型提示:需要配合tag字段的值,tag描述了一个当前任务状态下的一个node节点的索引。具体tag的编写方式请看下面一节"实现在节点树中定位控件"。
进度保存:手动进度保存是为了确保在任务中断后,游戏流程不受影响。 在招唤这个功能里,是只能召唤一次的,如果已经召唤成功了,服务器已经更新数据 ,后面的引导都是客户端的界面显示、关闭引导。如果在召唤之后,做一次进度保存,任务中断后再次进入引导会跳过这个任务组中的任务。
在理解了任务的功能后,需要有一个上层框架来一个一个的执行这些任务。
引导框架:在任务条件满足时(比如:等级要达到多少或者无任何条件),指示用户进行某项任务(比如按钮的点击)。当任务完成后,执行下一个任务,直接到全部任务被完成。它需要具有以下几点功能:
-
条件检查:检查是否该执行该任务,默认为无条件执行。这需要检查任务是否有onTaskBegan函数 ,不存在或返回ture才能执行任务指令
-
UI 定位:找到出当前任务中UI节点对应的矩形区。在指引任务中准确编写UI定位描述,由框架去检索UI节点,当检索到节点后调用任务的onLocateNode函数,传入节点对像,这可以让整个引导可以有更多的扩展。
-
触摸限制:屏蔽定位节点矩形区外的操作全部。
-
事件检查:矩形区对应的UI事件是否被执行。
-
任务完成:通知引导框架任务完成,进入下一个任务。
四、定位:实现在节点树中定位控件
以上几点中首要解决的是对UI控件的定位,对UI定位最直接有效的方法是在拿到这个UI控件对象,然后取出他的BoundingBox、锚点信息,进行座标转换。但如何才能拿到这个控件对象呢? 这里有两种实现方式:
1. 遍历场景树,把它搜索出来。
2. 事先把这个控件对象注册到你的引导框架中。
我采取的是第一种方法来定位控件,因为我不想在到处代码中添加游戏逻辑以外的东西。而且cocos2d-js中提供有现成的函数cc.helper.seekWidgetByName,如果你做的是手机游戏是不能直接使用这个函数的。在HTML5上这个函数可以遍历整个节点树,在jsb上只是遍历的Widget节点。 有两种方法解决这个问题:
1.把cc.helper.seekWidgetByName函数复制到自己代码文件中,重新取个名字叫:xxx.helper.seekNodeByName。在html5和jsb上都使用这个函数。
2.在c++ jsb上把cc.Helper.seekWidgetByName的参数修改成在Node节点上做遍历,或都重新封装一个jsb上的seekNodeByName函数 。
我这里偷懒还是使用第一种方法。通过上面的方法是否已经解决UI定位的问题呢?应该没那么简单吧!通过这种方法定位控件,就不用在引导配置文件里填写坐标或矩形数据,那是极其愚蠢的办法。
-
打住,他妈的还有问题!!!如果一个场景树中有两个相同节点名字怎搞?
这个问题确实问的很正确。因为我们经常会有名字相同的节点存在。比如下图:
如果他们名字都叫button,使用seekNodeByName是来定位控件的话只能找到其中一个。具体是那个是根据你addChild时
的顺序来决定的。这个问题如何解决?能唯一确定一个控件在场景树中的方法就是他的“完整路径”,
像这样一下来描述两个button:
"招唤界面/灵石招唤/召唤一次"
"招唤界面/仙玉招唤/召唤一次"
其实我们已经能定位到灵石招唤和仙玉招唤了(我这里为了方便理解使用中文名字)只需要这样写:
"灵石招唤/召唤一次"
"仙玉招唤/召唤一次"
这样也能精确定位到你想要的那个按钮。
在这里我实现了一个简易的定位器描述规则,我们以后通过以下方式在任务中定位一个控件 :
-
名字描述:在场景中有独一无二的名字时,直接描述控件名如:'_loginButton'。
-
路径名描述:在场景中需要定位的节点可能有重名时,找到其父节点,确保父节点不会有重名时使用:'parentName/button'。如果父节点也有重名,那就再向上使用其父节点名的父节点以此类推。
-
js属性描述: 有一种情况通过getChildByName无法直接访问的节点,如ccui.ScollView容器中的节点。我定义了一种简单的获取方式,例如 'layer1.button' 通过“.”这个符号来定位layer1下的一个属性为button。这种方式是在js中最为直接的方式。
-
子节点描述:使用完整路径描述一个控件时,有时会觉得比较长,例如:'mainLayer/layer1/button' 可以简写成 'mainLayer>button' 表示定位mainLayer下一个名字叫button的子节点,有可以是1级子节点,也可能为2、3、n级子节点。
5.复合描述:将以上几个方式组合使用,来描述一个控件:'mainLayer>homeLayer/layer1.button' 。用人话翻译下就是:mainLayer下有一个homeLayer子节点(不管是几级)下的一级子节点layer1下的一个变量名为button的节点.
描述符号总结 :
/ : 表示一级子节点
>: 表示一级~n级子节点
. : 表示属性名
这里就体现了为什么要注意名命规范的问题。有web开发经验的人一眼就能看出这里有一点css选择器的味道,呵呵!非常正确,正是借鉴了css选择器的思想,实现一个十分简单的选择器,我们这里可以称之为“定位器”,因为我们只需要定位出一个节点。
(未完待续)