本文上接前我一篇博文《使用cocos2d-js制作游戏新手引导(一)》
一、定位器的实现
定位器的目的是实现对场景树中的节点精确定位,获取对象实例,从而获取节点在界面中的位置、矩形大小等信息。
定位器:在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。
> var locator = "a/b.c#1"
> locator.split(/[.,//,>,#]/g);
[ 'a','b','c','1' ]
其实分隔符是用于修饰名字、tag、变量名的,一个定位符配合一个名字,于是设计一个简单的对象,如下:
{symbol: ‘/’,name:’a’}
代码如下
//使用正则表达示分隔名字
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’]即可。
/** * 定位节点 * @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.获取定位节点在世界中的位置和矩形大小
/** * 手形图标指向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中实现自动绑定cocostudioUI控件和事件(三)》
2.在引导层TouchBegan事件中屏蔽触摸操作
通常在引导过程中是不允许进行其它操作的,需要屏蔽所有UI行为,只能执行当前引导步骤规定的动作。我们通过之前的节点定位、座标转换、矩形区计算、遮罩显示一系列操作已经可以看到可操作区了。的区域。
使用cc.node的onTouchBegan事件在返回true后将触摸事件吞食掉,从而屏蔽下层事件。
onTouchBegan: function(touch) {
//触摸矩形区不存在,直接吞食事件
if (!this._touchRect) {
return true;
}
//获取触摸位置
var pt = touch.getLocation();
//检查触摸位置是否在可操作矩形区范围内
var ret = cc.rectContainsPoint(this._touchRect,pt);
if (ret && !this._touchNode) {
//隐藏手形
this._finger.setVisible(false);
//执行回调函数
this._callBack();
}
//在可操作区,不对屏蔽下层事件
return !ret;
},
五、定位区UI事件的检测
当引导层,将触摸事件放入下层游戏界面时,正常情况下会触发下层UI中的控件事件,从而进行真实的游戏步骤。这时我们可以简单地认为当前引导任务被完成。
但在真实的项目中却有不少问题,有时用户并不会按我们想象的操作进行游戏,在引导可操作区进行的不是点击而是滑动操作时就会非常的悲剧!因为这时引导上层已经检测到可操作已经发生触摸,将当前任务pass掉了,但下层UI事件并未执行,比如创建一个新界面,这时将导致引导进行不下去。
如何解决这个问题呢?如何检查下层UI已经真实进行了事件的触发?
目前我在自己的项目中没有特别好的办法,主要使用了sz.UILoader来管理事件并在控件的onTouchEnded时执行游戏逻辑,创建界面、界面切换等。 在sz.UILoader库中预留有勾子函数,用于拦截控件的事件。
sz.UILoader.prototype._onWidgetEvent = function(sender,type) {
if (type === ccui.Widget.TOUCH_ENDED) {
//使用观察者模式,发送按钮点击事件
xl.postMessage(xl.Message.BUTTON_CLICKED,sender);
}
};
xl.postMessage封装了cc.NotificationCenter,用于向xl.Message.BUTTON_CLICKED事件观察者广播消息,参数为当前控件对象。
利于观察者模式检查UI事件被执行
将引导层对象注册为xl.Message.BUTTON_CLICKED事件的观察者,一但有控件的ccui.Widget.TOUCH_ENDED事件被触发,引导层都能知道,注册代码如下:
xl.addobserver(xl.Message.BUTTON_CLICKED,this,this.touchNodeClicked);
touchNodeClicked为引导层观察者响应函数
touchNodeClicked: function(sender) {
if (this._touchNode &&
(sender === this._touchNode ||
sender.getName() === this._touchNode.getName() )) {
this.setTouchNode(null);
this._touchRect = null;
this._finger.setVisible(false);
//在此时才能执行任务回调函数,进行下一个任务的开始
this._callBack();
}
},
到此关于UI定位、提示动画、事件屏蔽与检查的所有细节已经全部完成。
(未完待继)