游戏开发中,一般而言说到新手引导时,可能很多人都会觉得头痛,觉得难以下手。尤其在游戏本身功能或需求还不稳定的情况,更是觉得难以下抓,然而非常不巧,本人就是在这种情况下接受了这样一个艰巨的任务。在定下心来深思一番之后,本人开始了引导功能的开发。在做的过程中碰到一些其实很有意思的东西,在这里分享给大家。

一、痛点:新手引导制作的难点及弊端

  • 需要在具有引导功能的代码单元插入引导代码或逻辑判断,干扰正常流程。

  • 引导代码的加入会影响原有的代码逻辑与流程,使代码变得复杂加大维护难度。

  • 界面或需求发生变化后引导功能需要大幅修改或重新制作。

  • 指引(手指提示)对应的矩形区定位麻烦,特别是需要适应不同尺寸屏幕的时候更加困难。

  • 编写引导配置文件也很头痛,需要策划、程序的高度配合。


二、期望:新手引导编程体验

笔者进入游戏开发应该说是手机游戏开发并不是很长时间,虽然参于过多个项目,但亲自编写新手引导这还是头一次。当时接到新人引导任务时,我们的项目只完成了:登录->主界面->抽卡->布阵->章节->关卡->战斗这样一个基本流程,界面美术、功能需求都极不稳定。但在公司的硬性要求下,冒着九死一生的危险开始了新手引导功能开发。在了解到传统的引导制作过程中的难点与弊端后,一直在思考没有更好的实现方式,我心中的引导编程的方式有以下几点:

  • 不需要在每个单元中去插入引导代码,游戏代码与引导代码应该尽量分离。本人很难忍受漂亮的代码被无情引导打乱,更难忍受本来糟糕的代码被引导弄得支离破碎。

  • 界面只发生简单UI位移、节点层次改变不需要修改引导代码。

  • 定位指引矩形区应该尽量的简单,且自适应不同尺寸屏幕。最好能做到策划人员都可以来制作部分流程引导。

  • 在引导需求明确、游戏功能正常的情况下,制作一个常规的引导步骤应该是非常快捷的,不会超过3分钟,快的话1分钟内就应该搞定(不是笔者说大话,确实已经实现)。


三、思想:引导功能的设计思路

在描述引导功有设计思路之前,有个重要的前题:命名规范


命名规范主要有两个方面:

  1. Cocos Studio中的控件名字

  2. 代码中动态创建的控件名字,以及类成员变量的名字。

在笔者的项目中使用了sw.UILoader来管理cocostudio的UI命名和事件。 

我们这里引入两个概念:任务与任务组。任务:把引导中的一个最小步骤称之为一个任务,比如提示点击某个按钮。任务组:把一系列的任务放在一个任务组中,当这个任务组中的任务全部完成,我们会保存一次任务进度。此时重新进入游戏将不会再执行这个任务,而是执行它的下一个任务组中的任务。可以理解任务组是引导中的一个步骤。


用json格式表示如:

{

    [{任务1},{任务2},{任务3}]

    [{任务7},{任务8},{任务9}]

    [{任务4},{任务5},{任务6}]

}

当从一个任务组中的任务中断后,再次进入引导 需要重新从这个任务组的第一个任务开始。

见下图演示了一个从主界面点击召唤->灵石召唤一次->点击获得->确定->仙玉召唤一次->点击获得->确定->点击空白退出召唤界面的流程。

js1.gif

上图演示的引导我分成两个任务组:灵石召唤、仙玉召唤。 任务配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
"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函数 ,不存在或返回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定位的问题呢?应该没那么简单吧!通过这种方法定位控件,就不用在引导配置文件里填写坐标或矩形数据,那是极其愚蠢的办法。

打住,还有问题!!! 如果一个场景树中有两个相同节点名字怎搞?

这个问题确实问的很正确。因为我们经常会有名字相同的节点存在。比如下图:

20150301092305852.jpg

如果他们名字都叫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级子节点。

  • 复合描述:将以上几个方式组合使用,来描述一个控件:'mainLayer>homeLayer/layer1.button' 。用人话翻译下就是:mainLayer下有一个homeLayer子节点(不管是几级)下的一级子节点layer1下的一个变量名为button的节点.


描述符号总结:

  • “/”: 表示一级子节点

  • “>”: 表示一级~n级子节点

  • “.”: 表示属性名

这里就体现了为什么要注意名命规范的问题。有web开发经验的人一眼就能看出这里有一点css选择器的味道,呵呵!非常正确,正是借鉴了css选择器的思想,实现一个十分简单的选择器,我们这里可以称之为“定位器”,因为我们只需要定位出一个节点。