很多场景中,我们常常遇到人物移动控制的情况。当两手拇指分别点按左右屏幕来控制人物左右移动的时候,非常容易发生点按频繁出现人物控制失灵的情况,这是因为两只手同时按的时候,会出现一只一方触摸失效的情况。今天就和大家分享一下我解决这个问题的办法。

今天重新建了一个工程,从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(event->getCurrentTarget());

//如果触摸点在屏幕右边,则往右移动,否则往左

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(event->getCurrentTarget());

//触摸停止,人物停止移动

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.gif

总结一下大概的思路:

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,所以,如果持续滑动的话,数值会不停地跳动。


2.gif

于是,根据这个规则,我们就可以来设计我们的优化方法了!

基本思路是,左右两个手指触摸开始,都会进行注册,左右手指离开,也都会分别取消注册。那么,如果两只手指同时按住,左右两个触摸都被注册了,如果右手离开,右边取消注册,此时检查左手是否注册,如果注册,说明左手还在触摸呢,人物就继续往左移动。反过来亦然,如果左手离开,检测是否右手还在触摸,如果是,则往右继续走。

好,既然思路确定了,就开始一步步实施计划!

首先,在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(event->getCurrentTarget());

//如果触摸点在屏幕右边,则往右移动,否则往左

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(event->getCurrentTarget());

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);

这就是最后的效果啦!


3.gif

等等!!又有新问题!

当单只手指从左滑向右,或者从右滑向左,人就会不停地往左,或者往右移动,而且拽也拽不回来!只有再次点击左或右边的屏幕,人物才会停止。


4.gif

这是由于我定义的注册机制的BUG,比如,从右滑到左边,右边注册了,而end是在左边,那么左边就是检测右边是否注册,是!于是就不同往右走了。反之亦然。

解决方法就是,在moved回调函数中,加入如下代码,限制移动范围:

void Man::touchMoved(cocos2d::Touch *touch, cocos2d::Event *event) {

auto man = reinterpret_cast(event->getCurrentTarget());

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;

}

}