【分享】实现平滑控制人物左右移动,手把手从0开始构建~~~~

当两手拇指分别点按左右屏幕来控制人物左右移动的时候,当点按频繁了容易出现人物不听使唤的情况,这是由于两只手同时按的时候,会出现一只一方触摸失效的情况。今天就和大家分享一下我解决这个问题的办法。

今天重新建了一个工程,从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<Man *>(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<Man *>(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 创建人物
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<Man *>(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<Man *>(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);

这就是最后的效果啦!

等等!!又有新问题!

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

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

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


void Man::touchMoved(cocos2d::Touch *touch, cocos2d::Event *event) {
    auto man = reinterpret_cast<Man *>(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;
    }
}

最后,给出我的源代码。希望朋友们多多支持!!顺便,我的版本是3.3final

http://pan.baidu.com/s/1kTJyswn

楼主很专业··点个赞··栗子很清晰····

我之前是学“状态机”的时候学过的

:14:

:14:感谢支持~~~