很多场景中,我们常常遇到人物移动控制的情况。当两手拇指分别点按左右屏幕来控制人物左右移动的时候,非常容易发生点按频繁出现人物控制失灵的情况,这是因为两只手同时按的时候,会出现一只一方触摸失效的情况。今天就和大家分享一下我解决这个问题的办法。
今天重新建了一个工程,从0开始,一步步制作人物移动的效果。
1、首先,我们需要定义一个人物的类
人物是我们移动的对象,这个类将人物的所有功能都包括进去。目前我们需要的功能仅仅是人能够移动。
由于主要是讲触摸功能的优化,所以Man类的创建就不多说了,贴出代码,各位自己看吧。
//man.h
class Man : public cocos2d::Node {
public:
static Man *create(const std::string &textureFile);
virtual bool init(const std::string &textureFile);
void moveRight(float delta);
private:
cocos2d::Sprite *sprite;
};
//man.cpp
Man *Man::create(const std::string &textureFile) {
Man *p = new Man;
if (p && p->init(textureFile)) {
p->autorelease();
}
else {
delete p;
p = nullptr;
}
return p;
}
bool Man::init(const std::string &textureFile) {
if (!Node::init())
return false;
//根据纹理名称创建精灵
sprite = Sprite::create(textureFile);
addChild(sprite);
return true;
}
void Man::moveRight(float delta) {
setPositionX(getPositionX() + delta);
}
2、创建人物,并设定触摸监听
人物建好了,我们需要设定触摸监听,目的是我们点击屏幕后,人物可以根据我们点击的方向进行移动。
首先,在HelloWorld的init函数中加入代码创建人物:
auto man = Man::create("man.png");
addChild(man);
man->setPosition(visibleSize / 2);
然后,同样在init函数中创立触摸监听,这里我们只需要用到单点触摸:
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::touchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::touchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::touchEnded, this);
listener->setSwallowTouches(true);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, man);
3、基本的框架搭得差不多了,下面我们就要构建细节了。
重点就在于监听事件的回调函数如何编写,我们需要在触摸开始时,人物开始移动,触摸结束时,人物停止移动。
我用的方法是,另外设立一个data member用来记录人物的状态,触摸回调函数来决定这个data member的值,而用一个schedule来检测这个data member根据它来对人物进行移动,或者立定。
首先在Man class definition中新增一个成员,叫manState:
enum {
STILL = 0,
RUN_LEFT = 1,
RUN_RIGHT = 2
};
int manState = STILL;
接下来,上面的方向分两步走,首先编写schedule:
我的方法是,在Man类中新增一个run函数,来根据manState决定man的运动,然后在调用它的类的update函数中,只需要运行run就可以了,比较简便:
void Man::run(float x) {
switch (manState) {
case STILL:
break;
case RUN_LEFT:
moveRight(-x); break;
case RUN_RIGHT:
moveRight(x); break;
}
}
这样子,run函数就会根据人物的manState状态值来决定人物的走向。
下面到了最关键的时刻,就是编写触摸事件:
首先,用最直观的方法编写触摸事件,然后再逐步修改完善。
bool HelloWorld::touchBegan(cocos2d::Touch *touch, cocos2d::Event *event) {
//获得目标
auto man = reinterpret_cast
//如果触摸点在屏幕右边,则往右移动,否则往左
if (touch->getLocation().x >= Director::getInstance()->getVisibleSize().width / 2) {
man->manState = Man::RUN_RIGHT;
}
else {
man->manState = Man::RUN_LEFT;
}
return true;
}
void HelloWorld::touchMoved(cocos2d::Touch *touch, cocos2d::Event *event) {
}
void HelloWorld::touchEnded(cocos2d::Touch *touch, cocos2d::Event *event) {
//获得目标
auto man = reinterpret_cast
//触摸停止,人物停止移动
man->manState = Man::STILL;
}
4、最后,在HelloWorld中enable schedule,在update函数中调用run函数。
需要一个额外的处理,就是将之前创建的man的声明放到类定义中,这样update函数就可以access到man了。
class HelloWorld : public cocos2d::Layer
{
//...
Man *man = nullptr;
virtual void update(float delta);
//...
};
bool HelloWorld::init() {
//...
scheduleUpdate();
//...
}
void HelloWorld::update(float delta) {
man->run(5);//5表示,每次调用移动5个像素,一秒钟算60次调用update的话就是300个像素每秒。
}
然后编译,成功后就是下面的样子啦,精灵比较粗糙 ^ ^!
总结一下大概的思路:
1 创建人物
2 创建监听
3 用一个变量作为中转站来表示人物方向
4 监听事件根据点击屏幕的位置来改变中转站的值
5 人物根据中转站的值决定自身移动的方向(run函数)
6 接通电源,通过schedule来激活动作~
到这里基本的实现的完成了,但这只是开始,本帖主要讲的在后面,关于如何优化触摸体验。目前的游戏运行时,如果先按住左,然后左不放手,按住右,人物会往右走,但是,如果放手右手,左手不放手,人物不会再往左走,只会傻在那里!
怎么让触摸更加流畅
从上面可以看到,目前实现的触摸还是比较生硬的,下面就来讲讲怎么让触摸更加流畅。
着重要优化的就是我们的三个监听回调函数,即决定manState值得3个函数。
首先,列示出所有可能出现的操作:
1 单手按左边或右边
2 两只手同时按住,之后松开左手
3 两只手同时按住,之后松开右手
第一种情况目前已经实现,现在要实现的是第二和第三种情况,我需要两手同时按住然后松开左手的时候人物会继续往右走,或者松开右手的时候,人物会往左继续走。
经过我的测试,左手先按住不放,began会被调用一次(人物开始往左移动),如果期间手指滑动,则moved会被不断调用,而此时右手按上去之后,began会再次被调用(现在人物开始往右移动),这是如果左手或右手移动,moved函数都会被调用。而如果此时松开左手或右手,ended会被调用一次,松开另外一只手,ended仍然会被调用一次。所以,看似的单点触摸其实是可以识别多个触点的。
这是我的测试结果,我在其中加入了一个label,began moved ended都会设置这个label的string,而moved更是多设置了一个数值,用来检查是否持续在被调用,每次调用+1,所以,如果持续滑动的话,数值会不停地跳动。
于是,根据这个规则,我们就可以来设计我们的优化方法了!
基本思路是,左右两个手指触摸开始,都会进行注册,左右手指离开,也都会分别取消注册。那么,如果两只手指同时按住,左右两个触摸都被注册了,如果右手离开,右边取消注册,此时检查左手是否注册,如果注册,说明左手还在触摸呢,人物就继续往左移动。反过来亦然,如果左手离开,检测是否右手还在触摸,如果是,则往右继续走。
好,既然思路确定了,就开始一步步实施计划!
首先,在Man类中创建两个值,用来表示左右的注册:
class Man : public cocos2d::Node {
//...
public:
bool registerLeft = false;
bool registerRight = false;
};
接着修改触摸回调函数:
bool Man::touchBegan(cocos2d::Touch *touch, cocos2d::Event *event) {
//获得目标
auto man = reinterpret_cast
//如果触摸点在屏幕右边,则往右移动,否则往左
if (touch->getLocation().x >= Director::getInstance()->getVisibleSize().width / 2) {
man->manState = Man::RUN_RIGHT;
man->registerRight = true;
}
else {
man->manState = Man::RUN_LEFT;
man->registerLeft = true;
}
return true;
}
void Man::touchMoved(cocos2d::Touch *touch, cocos2d::Event *event) {
}
void Man::touchEnded(cocos2d::Touch *touch, cocos2d::Event *event) {
//获得目标
auto man = reinterpret_cast
if (touch->getLocation().x >= Director::getInstance()->getVisibleSize().width / 2) {
//如果右手松开,则检查是否左手仍在触摸,如果是,则往左走,如果不是,则停止
if (man->registerLeft) {
man->manState = Man::RUN_LEFT;
}
else {
man->manState = Man::STILL;
}
//取消右手的注册
man->registerRight = false;
}
else {
//如果左手松开,则检查是否右手仍在触摸,如果是,则往右走,如果不是,则停止
if (man->registerRight) {
man->manState = Man::RUN_RIGHT;
}
else {
man->manState = Man::STILL;
}
//取消左手手的注册
man->registerLeft = false;
}
}
这样我们就实现了流畅的触摸!等等!!
细心的朋友可能已经发现,这三个回调函数的变成了Man::开头,成为了Man类的函数了,怎么回事?
为了方便起见,我把监听功能和schedule功能都搬家到了Man类中,并把一些不需要抛头露面的成员都从public搬到了protected中。这样子,HelloWorld就只需要创建一个Man的object就可以实现人物的移动啦!
现在的HelloWorld仅仅只有一段代码,感觉好舒服~~!
//create the man
man = Man::create("man.png");
addChild(man);
man->setPosition(visibleSize / 2);
这就是最后的效果啦!
等等!!又有新问题!
当单只手指从左滑向右,或者从右滑向左,人就会不停地往左,或者往右移动,而且拽也拽不回来!只有再次点击左或右边的屏幕,人物才会停止。
这是由于我定义的注册机制的BUG,比如,从右滑到左边,右边注册了,而end是在左边,那么左边就是检测右边是否注册,是!于是就不同往右走了。反之亦然。
解决方法就是,在moved回调函数中,加入如下代码,限制移动范围:
void Man::touchMoved(cocos2d::Touch *touch, cocos2d::Event *event) {
auto man = reinterpret_cast
if (touch->getLocation().x >= Director::getInstance()->getVisibleSize().width / 2 && registerLeft && !registerRight) {
//如果从左向右滑动超过了屏幕的一半,则停止,并取消左手注册
man->manState = Man::STILL;
man->registerLeft = false;
}
else if (touch->getLocation().x < Director::getInstance()->getVisibleSize().width / 2 && registerRight && !registerLeft) {
//如果从右向左滑动超过了屏幕的一半,则停止,并取消右手注册
man->manState = Man::STILL;
man->registerRight = false;
}
}