在上一个教程中,我们讲到了制作游戏新手引导的难点,提到了制作中的命名规范和定位问题。(参见用Cocos2d-JS制作游戏新手引导教程1

下面继续:

一、定位器的实现

定位器的目的是实现对场景树中的节点精确定位,获取对象实例,从而获取节点在界面中的位置、矩形大小等信息。 

定位器:在Cocos2d(js)游戏引擎中用于精确描述场景树中的某一节点的字符串,其实现方式借鉴了css(层叠样式表)选择器设计思路,以下我们将实现一个简单的从定位器字符串解析到节点定位的整个过程。


1.定位符规则

在Cocos2d中可以通过节点名字、节点tag值来表示一个节点,在js中还可以使用对象的变量名比如:this[‘_button’]来获取节点对象。 一共有三种有效方式来表示一个node节点对象,于是这里对应三种定位符号,如下:

  • “/” :名字(name)定位符,例如: ‘a/b/c’ 、’dialogLayer/_closeButton’ 

  • “#”:tag(id)定位符,例如:’a#123’ 

  • “.”:变量名(var)定位符,例如:’a._okButton’

还有为了简化定位器字符串的长度,借鉴css中的子选择器

  • “>”:子(child)定位符,例如:’a>c’


2.定位器解析

定位器字符串中只存在名字、tag、变量名、定位符,其中由定位符将名字、tag、变量名隔开。在js中最简单的就是使用String.split函数将其分开,但这里分隔符(/、#、. 、>)不止一个符号如何实现呢?之前我是自己写的一个遍历函数来解析,但感觉有些丑陋。思考之后觉得split不应该不支持多个分隔符,于是搜索了下,发现果真不出我所料splite还支持正则表达示的分隔规则,代码由n行变成1行,非常满意,越来越喜欢上了js。

1
2
3
 > var locator = "a/b.c#1"
 > locator.split(/[.,//,>,#]/g);
 [ 'a', 'b', 'c', '1' ]

其实分隔符是用于修饰名字、tag、变量名的,一个定位符配合一个名字,于是设计一个简单的对象,如下:

1
{symbol: ‘/’, name:’a’}

代码如下:

1
2
3
4
5
6
7
//使用正则表达示分隔名字
var names = str.split(/[.,//,>,#]/g);
var segments = names.map(function(name) {
    var index = str.indexOf(name);
    var symbol = str[index - 1] || '>';
    return {symbol: symbol, name: name.trim()};
});

segments中就是我们需要的东西了,而且这里我们为了编写方便或美观,在定位符与名字之间允许有空格,如:”a > b # 1” 

还有通常第一段定位符通常为主界面下的某个子节点,我这里使用’>’为默认定位符。


3.定位函数实现细节

有了上面定位器字符串的解析输出,定位其实是很容易的,因为cocos2d-js中已经提供了getChildByName、getWidgetByTag、seekWidgetByName、seekWidgetByTag,而对于变量定位符则更是简单,object[‘name’]即可。

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
43
44
45
/**
* 定位节点
* @param locator      定位器字符串
* @param cb           回调函数
* @returns null/node  返回值
*/
locateNode: function(locator, cb) {
    //解析定位器字符串
    var segments = this.parseLocatorString(locator);
    if (_.isEmpty(segments)) {
        return;
    }
    cc.log("定位器:" + locator);
    var child, 
        node = this._target;  //this._target为检索起点节点
    for (var i = 0; i < segments.length; i++) {
        var item = segments[i];
        switch (item.symbol) {
            case '/':
                child = node.getChildByName(item.name); break;
            case '.':
                child = node[item.name]; break;
            case '>':
                child = xl.UIHelper.seekNodeByName(node, item.name); break;
            case '#':
                child = xl.UIHelper.seekNodeByTag(node, item.name); break;
        }
        if (child) {
            node = child
        } else {
            node = null;
            break;
        }
    }
    if (node) {
        cb(node); //定位节点成功,回调返回结果
        this._locatedNode = node;
    } else {
        //定位失败,等待0.1秒后重试。
        this.scheduleOnce(function () {
            this.locateNode(locator, cb);
        }, 0.1);
    }
    return node;
}

以上代码实现了在场景树中定位检索的过程,自认代码还算清晰明了,也很简单。在代码最后一段中,当定位失败后,会启动定时器再次检索节点,这是为了解决在引导任务切换时UI界面还没有创建出来而导致定位设计的解决方法。


二、手形提示动画与坐标转换

当我们在场景树中定位到节点获取到节点对象后,就可以通过节点属性获取它的位置、大小、描点等信息,从而计算出节点在屏幕上的位置。


1.节点位置与世界位置

position: 我们可以通过node.getPosition()、node.setPosition()来获取和设置节点在其父节点中的位置,也可以使用属性node.x、node.y。这里需要注意的是一个节点的座标只是表示他在父节点位置,我们在大多数时候,节点是层层包含的。我们要获取一个节点在屏幕中的位置不能简单地使用x\y属性。 

世界座标:在Cocos2d中所有节点都提示了从:局部座标到世界座标的相互转换,函数为 node.convertToNodeSpace 、node.convertToWorldSpace. 需要注意的是我们要获取一个节点所在的世界座标位置,需使用其父节点计算子节点在世界中的位置。


2.获取定位节点在世界中的位置和矩形大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 /**
 * 手形图标指向node节点
 * @param node   节点对象
 * @param cb     手指点击后的回调完成函数
 */
pointToNode: function(node, cb) {
    this.setTouchNode(null);
    var pt = node.getParent().convertToWorldSpace(node.getPosition());
    //设置手指图标,指定向pt位置
    this.setFinger(pt);
    //通过node锚点计算,矩形大小
    pt.x -= node.width * node.anchorX;
    pt.y -= node.height * node.anchorY;
    this._touchRect = cc.rect(pt.x, pt.y, node.width, node.height);
    //开启遮罩显示   
    this.showMask();
    //保存回调函数,node节点事件完成后执行
    this._callBack = cb;
},


3.手形提示动画

手形提示动画非常简单,使用action动作 cc.MoveTo即可完成,只不过在这里setFighter函数我们有时传入一个point参数,有时可能传入的是一个point数组。当传入一个point数组时,希望手形精灵按照数组中的point位置一个一个的依次移动。


三、定位区遮罩显示

我们获取到节点对象,世界座标位置、矩形大小这些信息,生成一个矩形遮罩非常容易。遮罩显示主要使用cocos2d中的ClippingNode来实现,关于ClippingNode相关的技术、教程、文章已经有很多了,这里就不在详细说明,等我把代码整理好后会提供开打、显示遮罩的开关已方便使用。


四、非定位区触摸事件屏蔽

1.为引导层注册触摸事件

关于为Node节点注册触摸事件请参考:《在Cocos2d-JS中实现自动绑定Cocos Studio UI控件和事件(二)


2.在引导层TouchBegan事件中屏蔽触摸操作

通常在引导过程中是不允许进行其它操作的,需要屏蔽所有UI行为,只能执行当前引导步骤规定的动作。我们通过之前的节点定位、座标转换、矩形区计算、遮罩显示一系列操作已经可以看到可操作区了。这里写图片描述的区域。

QQ截图20150312112401.jpg

使用cc.node的onTouchBegan事件在返回true后将触摸事件吞食掉,从而屏蔽下层事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
onTouchBegan: function(touch) {
    //触摸矩形区不存在,直接吞食事件
    if (!this._touchRect) {
        return true;
    }
    //获取触摸位置
    var pt = touch.getLocation();
    //检查触摸位置是否在可操作矩形区范围内
    var&n
0