用Cocos2d-JS制作游戏新手引导教程2
在上一个教程中,我们讲到了制作游戏新手引导的难点,提到了制作中的命名规范和定位问题(参见用Cocos2d-JS制作游戏新手引导教程1)。下面我们继续深入讲解。
一、定位器的实现
定位器的主要目的是在场景树中对节点进行精确定位,获取对象实例,进而获取节点在界面中的位置、矩形大小等信息。在Cocos2d(js)游戏引擎中,定位器是用于精确描述场景树中某一节点的字符串,其实现方式借鉴了CSS(层叠样式表)选择器的设计思路。接下来,我们将详细介绍从定位器字符串解析到节点定位的整个过程。
1. 定位符规则
在Cocos2d中,有三种有效方式可以表示一个node节点对象,分别对应三种定位符号:
- “/” :名字(name)定位符,例如:
'a/b/c'、'dialogLayer/_closeButton'。 - “#”:tag(id)定位符,例如:
'a#123'。 - “.”:变量名(var)定位符,例如:
'a._okButton'。
此外,为了简化定位器字符串的长度,我们借鉴了CSS中的子选择器,引入了 “>” 作为子(child)定位符,例如:'a>c' 。
2. 定位器解析
定位器字符串中包含名字、tag、变量名和定位符,定位符用于分隔名字、tag和变量名。在JavaScript中,我们可以使用String.split函数来实现分隔。然而,分隔符(/、#、. 、>)不止一个,该如何处理呢?最初,我自己编写了一个遍历函数来解析,但代码显得比较繁琐。经过思考,我意识到split函数应该支持正则表达式的分隔规则,搜索后发现确实如此。使用正则表达式后,代码从多行简化为一行,非常简洁,这让我更加喜欢JavaScript了。
以下是示例代码:
var locator = "a/b.c#1";
var result = locator.split(/[.,\/,>,#]/g);
console.log(result); // [ 'a', 'b', 'c', '1' ]
实际上,分隔符是用于修饰名字、tag和变量名的,一个定位符配合一个名字。为了方便处理,我们设计一个简单的对象来存储解析结果:
{symbol: '/', name: 'a'}
下面是完整的定位器解析代码:
function parseLocatorString(str) {
// 使用正则表达式分隔名字
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()};
});
return segments;
}
在解析过程中,我们允许定位符与名字之间存在空格,例如:"a > b # 1" 。并且,通常第一段定位符默认使用> ,表示主界面下的某个子节点。
3. 定位函数实现细节
有了定位器字符串的解析结果,定位节点就变得相对容易了。因为Cocos2d-JS中已经提供了getChildByName、getWidgetByTag、seekWidgetByName、seekWidgetByTag等方法,对于变量定位符,直接使用object['name']即可。
以下是定位节点的函数代码:
function locateNode(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. 节点位置与世界位置
在Cocos2d中,我们可以使用node.getPosition()、node.setPosition()来获取和设置节点在其父节点中的位置,也可以使用属性node.x、node.y。需要注意的是,节点的坐标表示的是它在父节点中的位置,而大多数情况下节点是层层嵌套的,因此不能简单地使用x、y属性来获取节点在屏幕中的位置。
Cocos2d提供了从局部坐标到世界坐标的相互转换函数:node.convertToNodeSpace 和 node.convertToWorldSpace。要获取一个节点所在的世界坐标位置,需要使用其父节点来计算子节点在世界中的位置。
2. 获取定位节点在世界中的位置和矩形大小
以下是获取定位节点在世界中的位置和矩形大小的函数:
function pointToNode(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. 手形提示动画
手形提示动画可以使用cc.MoveTo动作来实现。setFighter函数有时传入一个point参数,有时传入一个point数组。当传入point数组时,手形精灵会按照数组中的point位置依次移动。
三、定位区遮罩显示
在获取到节点对象、世界坐标位置和矩形大小等信息后,生成一个矩形遮罩相对容易。遮罩显示主要使用Cocos2d中的ClippingNode来实现。关于ClippingNode的相关技术、教程和文章已经有很多了,这里不再详细说明。等我整理好代码后,会提供开启、显示遮罩的开关,方便使用。
四、非定位区触摸事件屏蔽
1. 为引导层注册触摸事件
关于为Node节点注册触摸事件,请参考:《在Cocos2d-JS中实现自动绑定Cocos Studio UI控件和事件(二)》
2. 在引导层TouchBegan事件中屏蔽触摸操作
在引导过程中,通常不允许进行其他操作,需要屏蔽所有UI行为,只允许执行当前引导步骤规定的动作。我们可以通过之前的节点定位、坐标转换、矩形区计算和遮罩显示等操作,确定可操作区域。
以下是在引导层TouchBegan事件中屏蔽触摸操作的代码:
function onTouchBegan(touch) {
// 触摸矩形区不存在,直接吞食事件
if (!this._touchRect) {
return true;
}
// 获取触摸位置
var pt = touch.getLocation();
// 检查触摸位置是否在可操作矩形区范围内
// 此处原代码未完整,应补充判断逻辑
// ...
}
通过上述步骤,我们可以实现游戏新手引导中的定位、提示动画、遮罩显示和触摸事件屏蔽等功能。