cocos2dx游戏开发学习——虚拟摇杆(8方向)讲解

写这篇博客的目的主要是记录一下 虚拟摇杆的实现过程。虚拟摇杆一般分文四方向和八方向,也主要根据项目需求来决定。直接进入主题吧。

先上效果图:


这里写图片描述

  • 方向的思路分析
    这里写图片描述

    看图,说先我们可以将8个方向在坐标系中画出来,这里涉及到余弦定理,高中数学可能经常看到这样的图,从图中可以很明确的看出每45°一个方向。这里我们只需要根据a的度数来判别方向。(例如:022.5°和337.5°360° 是右方向,67.5112.5是上方向,等等)。那么问题来了,怎么去算出a的角度呢,根据反余弦定义,我们一般只能算出0180°,那如何才能得到0~360°呢?这里隐约记得当a大于180度时候,a的余弦值等于(360-a)的余弦(如果不熟悉的话记得补一补高中数学了)。这样就解决了方向问题啦。
    上代码:
/**
 * 获取以p1为圆心,p2p1与x轴的弧度值
 */
float GameRocker::getRad(cocos2d::Point p1, cocos2d::Point p2)
{
    float xx = p2.x - p1.x;
    float yy = p2.y - p1.y;
    // 斜边
    float xie = sqrt(pow(xx, 2) + pow(yy, 2));
    // yy >= 0 弧度在于 0 到 π 之间。(0~180°)
    // yy < 0 弧度在于 π 到 2π 之间。(180°~360°)
    float rad = yy >= 0 ? (acos(xx / xie)) : (PI * 2 - acos(xx / xie));
    return rad;
}

  • 虚拟摇杆类的实现
    有一个问题需要思考,这个遥感类应该继承Node呢还是Layer,为什么我会考虑这个,因为我看到一些遥感类都是继承Layer,而我一般喜欢继承Node。而且我觉得继承Node要比继承Layer更加灵活(这个问题希望大牛过来指点)。
    无论继承Node还是Layer还是其他,我们都需要添加触摸监听事件。
    很多游戏中,我们发现虚拟摇杆会有个额外状态,常见的就是加速,玩儿过手机版的2K的应该熟悉。我也考虑做类似的功能,还有许多游戏中摇杆可能不是固定的。这里我就直接上代码了。

GameRocker.h

/**
 *  摇杆状态类
 */
class GameRockerState
{
public:
    
    /**
     *  摇杆方向枚举
     */
    enum class DirectionType
    {
        StayType,
        LeftType,
        LeftUpType,
        UpType,
        RightUpType,
        RightType,
        RightDownType,
        DownType,
        LeftDownType,
    };
    
    /**
     *  速度状态
     */
    enum class SpeedState
    {
        StayState,
        NormalState,
        QuickState,
    };
    
public:
    GameRockerState():_direction(GameRockerState::DirectionType::StayType),_speedState(GameRockerState::SpeedState::StayState) {};
    GameRockerState(const GameRockerState& state){
        _direction = state.getDirection();
        _speedState = state.getSpeedState();
    };
    ~GameRockerState(){};
    
    GameRockerState operator = (const GameRockerState& state)
    {
        if(this == &state) return *this;
        _direction = state.getDirection();
        _speedState = state.getSpeedState();
        return *this;
    };
    
    bool operator == (const GameRockerState& state)
    {
        return (_direction == state.getDirection() && _speedState == state.getSpeedState());
    }
    
    bool operator != (const GameRockerState& state)
    {
        return !(*this==state);
    }

protected:
    CC_SYNTHESIZE_PASS_BY_REF(GameRockerState::DirectionType,_direction,Direction);
    CC_SYNTHESIZE_PASS_BY_REF(GameRockerState::SpeedState,_speedState,SpeedState);
};





/**
 *  摇杆类
 */
class GameRocker:public cocos2d::Node
{
public:
    typedef std::function<void(const GameRockerState&)> StateChangeCallback;
    
    /**
     *  速度状态
     */
    enum class KeysType
    {
        Key_4,// 四键位
        Key_8,// 八键位
    };
    
    
public:
    GameRocker();
    ~GameRocker();
    static GameRocker* createWithFilename(const std::string& barFilename,const std::string& bgFilename);
    virtual const cocos2d::Size& getContentSize()const;
    
    /**
     *  设置状态改变的监听回调
     */
    void setupDirectionStateCallback(const StateChangeCallback& callback);
    
    /**
     *  获取当前摇杆状态
     */
    inline const GameRockerState& getRockerState() const { return _rockerState; };
    
    
protected:
    bool initWithFilename(const std::string& barFilename,const std::string& bgFilename);
    
    virtual void onEnter() override;
    
    void addTouchEvent();
    
    void changeRockerState(const GameRockerState& state);
    
    float getRad(cocos2d::Point p1,cocos2d::Point p2);

private:
    cocos2d::Sprite* rockerBar;
    cocos2d::Sprite* rockerBG;
    
    /** 摇杆的正常活动半径 (指用于正常跑)*/
    float _normalRadius;
    /** 摇杆的加速活动半径(指用于快跑) */
    float _quickRadius;
    
    
    /** 摇杆状态 */
    GameRockerState _rockerState;
    /** 摇杆状态改变的回调函数 */
    StateChangeCallback _changeCallback;
    //** 最初的坐标 *//
    cocos2d::Vec2 _initPosition;
    
    /** 摇杆是否能够使用 */
    CC_SYNTHESIZE(bool,_enable,Enable);
    /** 摇杆是否是可移动 */
    CC_SYNTHESIZE(bool, _movable, Movable);
    /** 可移动的范围,GL坐标系 */
    CC_SYNTHESIZE_PASS_BY_REF(cocos2d::Rect,_moveRect,MoveRect);
    /** 键位方向 */
    CC_SYNTHESIZE(GameRocker::KeysType,_keysType,KeysType);
    
};

GameRocker.cpp

#define PI 3.141592654

GameRocker::GameRocker(){}

GameRocker::~GameRocker(){}


GameRocker* GameRocker::createWithFilename(const std::string &barFilename, const std::string &bgFilename)
{
    GameRocker* rocker = new (std::nothrow) GameRocker();
    if (rocker && rocker->initWithFilename(barFilename, bgFilename)) {
        rocker->autorelease();
        return rocker;
    }else{
        CC_SAFE_DELETE(rocker);
        return nullptr;
    }
}


const Size& GameRocker::getContentSize() const
{
    if (rockerBG) {
        return rockerBG->getContentSize();
    }
    return Size::ZERO;
}

void GameRocker::setupDirectionStateCallback(const GameRocker::StateChangeCallback &callback)
{
    _changeCallback = callback;
}


bool GameRocker::initWithFilename(const std::string &barFilename, const std::string &bgFilename)
{
    if (Node::init())
    {
        rockerBG = Sprite::create(bgFilename);
        rockerBar = Sprite::create(barFilename);
        rockerBar->setPosition(rockerBG->getContentSize().width/2,rockerBG->getContentSize().height/2);
        rockerBG->addChild(rockerBar);
        this->addChild(rockerBG);
        setContentSize(rockerBG->getContentSize());
        _normalRadius = rockerBG->getContentSize().width/2;
        _quickRadius = rockerBG->getContentSize().width * 0.65;
        _enable = true;
        _movable = false;
        _moveRect = Rect(0, 0, 0, 0);
        _keysType = GameRocker::KeysType::Key_4;
        addTouchEvent();
        return true;
    }
    return false;
}


void GameRocker::onEnter()
{
    Node::onEnter();
    _initPosition = this->getPosition();
}


void GameRocker::addTouchEvent()
{
    auto listener = EventListenerTouchOneByOne::create();
    listener->setSwallowTouches(false);
    listener->onTouchBegan = [this](Touch* touch, Event* evnt)->bool
    {
        if (_movable)
        {
            if (_moveRect.containsPoint(touch->getLocation()))
            {
                this->setPosition(touch->getLocation());
                return true;
            }
            return false;
        }else{
            Rect rockerRect = Rect(this->getPositionX() - _normalRadius, this->getPositionY() - _normalRadius, _normalRadius*2, _normalRadius*2);
            Rect rect = this->getBoundingBox();
            rect.origin = rect.origin - Vec2(rockerBG->getContentSize().width/2,rockerBG->getContentSize().height/2);
            return rockerRect.containsPoint(touch->getLocation());
        }
    };
    
    listener->onTouchMoved = [this](Touch* touch, Event* evnt)
    {
        if (!_enable) return;

        // 获取角度
        Point p1 = this->getPosition();
        Point p2 = touch->getLocation();
        float rad = getRad(p1, p2);
        // 用户触摸点到摇杆的中心的距离
        float touchRadius = sqrt(pow(touch->getLocation().x - this->getPositionX(), 2) + pow(touch->getLocation().y - this->getPositionY(), 2));
        
        float radius = MIN(_normalRadius, touchRadius);
        
        GameRockerState tempState;
        if (touchRadius >= _quickRadius) {
            // 加速状态
            radius = _quickRadius;
            rockerBG->setColor(Color3B::RED);
            tempState.setSpeedState(GameRockerState::SpeedState::QuickState);
        }else{
            // 普通状态
            rockerBG->setColor(Color3B::WHITE);
            tempState.setSpeedState(GameRockerState::SpeedState::NormalState);
        }
        // 设置 摇杆的位置
        Point barPoint = Vec2(radius * cos(rad), radius * sin(rad)) + Vec2(rockerBG->getContentSize().width/2,rockerBG->getContentSize().height/2);
        rockerBar->setPosition(barPoint);
        // 弧度转化成脚步
        float angle = 180.f / PI * rad;
        if (_keysType == GameRocker::KeysType::Key_4) {
            if ((angle >= 0 && angle < 45) || (angle >= 315 && angle < 360)) {//右
                tempState.setDirection(GameRockerState::DirectionType::RightType);
            }
            if (angle >= 45 && angle < 135) { //上
                tempState.setDirection(GameRockerState::DirectionType::UpType);
            }
            if (angle >= 135 && angle < 225) { //左
                tempState.setDirection(GameRockerState::DirectionType::LeftType);
            }
            if (angle >= 225 && angle < 315) { //下
                tempState.setDirection(GameRockerState::DirectionType::DownType);
            }
        }
        if (_keysType == GameRocker::KeysType::Key_8) {
            if ((angle >= 0 && angle < 22.5) || (angle >= 337.5 && angle < 360)) {//右
                tempState.setDirection(GameRockerState::DirectionType::RightType);
            }
            if (angle >= 22.5 && angle < 67.5) { //右上
                tempState.setDirection(GameRockerState::DirectionType::RightUpType);
            }
            if (angle >= 67.5 && angle < 112.5) { //上
                tempState.setDirection(GameRockerState::DirectionType::UpType);
            }
            if (angle >= 112.5 && angle < 157.5) { //左上
                tempState.setDirection(GameRockerState::DirectionType::LeftUpType);
            }
            if (angle >= 157.5 && angle < 202.5) { //左
                tempState.setDirection(GameRockerState::DirectionType::LeftType);
            }
            if (angle >= 202.5 && angle < 247.5) { //左下
                tempState.setDirection(GameRockerState::DirectionType::LeftDownType);
            }
            if (angle >= 247.5 && angle < 292.5) { //下
                tempState.setDirection(GameRockerState::DirectionType::DownType);
            }
            if (angle >= 292.5 && angle < 337.5) { //右下
                tempState.setDirection(GameRockerState::DirectionType::RightDownType);
            }
        }
        
        // 改变摇杆Bar的状态
        changeRockerState(tempState);
    };
    auto touchEndCallback = [this](Touch* touch, Event* evnt)
    {
        CCLOG("Layer touch  move  (%f,%f)",touch->getLocation().x,touch->getLocation().y);
        rockerBar->setPosition(rockerBG->getContentSize().width/2,rockerBG->getContentSize().height/2);
        rockerBG->setColor(Color3B::WHITE);
        this->setPosition(_initPosition);
        GameRockerState tempState;
        changeRockerState(tempState);
    };
    listener->onTouchEnded = touchEndCallback;
    listener->onTouchCancelled = touchEndCallback;
    
    Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
}


void GameRocker::changeRockerState(const GameRockerState &state)
{
    if (_rockerState == state) return;
    _rockerState = state;
    if (_changeCallback) _changeCallback(_rockerState);
}


/**
 * 获取以p1为圆心,p2p1与x轴的弧度值
 */
float GameRocker::getRad(cocos2d::Point p1, cocos2d::Point p2)
{
    float xx = p2.x - p1.x;
    float yy = p2.y - p1.y;
    // 斜边
    float xie = sqrt(pow(xx, 2) + pow(yy, 2));
    // yy >= 0 弧度在于 0 到 π 之间。(0~180°)
    // yy < 0 弧度在于 π 到 2π 之间。(180°~360°)
    float rad = yy >= 0 ? (acos(xx / xie)) : (PI * 2 - acos(xx / xie));
    return rad;
}

最后提一句,这个虚拟键盘可拓展的东西很多,大家可以发挥一下。另外有什么不足的地方,大家可以提出来

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容