Cocos2d-x塔防游戏_贼来了10——选择关卡

原创: 任珊


Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了9——关卡数据

综述

目前我们的塔防游戏已经算是有模有样了,接下来在最后一部分的代码中,我们将为这款贼来了游戏锦上添花,为它添加选关功能、预加载功能、音乐音效以及一些不同的游戏场景和粒子渲染效果。

该游戏的完整代码已更新,大家可以点击这里进行下载。

本章教程我们将会先教大家实现以下内容:

  • 滑动式UI界面。左右滑动界面实现不同“场景”的切换。
  • 关卡解锁模式和选关功能。根据玩家所选择的关卡进入不同游戏场景,当第 N 关成功过关的时候,解锁第 N+1 关。

滑动式UI界面

UI界面能直观的指引玩家进行游戏操作,好的UI设计可以吸引玩家,提升一款游戏的品味。所以本款游戏将采用交互性很强的滑动式UI界面,同时结合一些粒子特效,为用户提供更友好的界面设计。

那如何实现滑动式的UI啦!其实在Cocos2d-x 3.0中已经封装了可供用户实现滚动操作的ScrollView控件类,但它的功能还不是特别强大,使用起来不是很理想,因为它只可以实现简单的滑动效果,不能满足我们的需要(滑动一下刚好滑动到指定的下一个位置),所以这里我们必须重写一个类来实现滚动操作,设计思路如下:

我们定义一个继承于Layer的LevelLayer类,用它来作为滑动页面的容器层,其可视化大小固定。再定义一个类,继承于Node,根据使用需要依次把这个类的对象作为LevelLayer类的子节点添加到其上,间隔宽度(高度)为LevelLayer的宽(高)。在LevelLayer中实现触摸监听,如果是滑动事件,就执行滚屏的操作,并且在触摸事件完成后跳转到当前子节点的位置;如果是点击事件,则交由当前子节点处理。最后再定义一个场景,把滚动层添加到其上。层级关系如下:

容器层

下面我们先来看看如何定义作为滑动页面的容器层(LevelLayer)。先看代码:

class LevelLayer: public Layer
{
private:    
    int pageNode;
    int curPageNode;
    Point touchDownPoint;
    Point touchUpPoint;
    Point touchCurPoint;
    float WINDOW_WIDTH;
    float WINDOW_HEIGHT;
    void goToCurrNode();

public:
    LevelLayer();
    ~LevelLayer();

    virtual bool init();
    static cocos2d::Scene* createScene();
    CREATE_FUNC(LevelLayer);

    void addNode(Node *level);
    void menuCloseCallback(Ref* pSender);
    bool onTouchBegan(Touch *pTouch, Event  *pEvent);
    void onTouchMoved(Touch *pTouch, Event  *pEvent);
    void onTouchEnded(Touch *pTouch, Event  *pEvent);  
};

在该类中pageNode属性是LevelLayer层中总共的子页数,curPageNode表示当前显示的第几个页节点,0表示第1页,1表示第2页,依次类推。属性touchDownPoint、touchUpPoint、touchCurPoint分别用来记录触摸屏幕的按下点、触摸屏幕抬起点和当前触摸点,我们将通过它们计算并控制LevelLayer层的滚动。WINDOW_WIDTH、WINDOW_HEIGHT是LevelLayer的固定宽高。

goToCurrNode方法在触摸事件完成后根据当前滑动偏移量跳转到当前子页面,addNode方法用于向LevelLayer中添加子节点。下面是它们的实现方法:

void LevelLayer::addNode(Node *level)
{
    if (level)
    {
        level->setContentSize(Size::Size(WINDOW_WIDTH, WINDOW_HEIGHT));
        level->setPosition(Point(WINDOW_WIDTH * pageNode, 0));
        this->addChild(level);
        pageNode++;
    }
}

void LevelLayer::goToCurrNode()
{
    this->runAction(MoveTo::create(0.4f, Point::Point(-curPageNode * WINDOW_WIDTH, 0)));
}

addNode方法添加一个节点到LevelLayer上。节点的大小固定,位置依次向后排开,所以当有多个节点添加到LevelLayer时,它们就像排队照相的人一样,谁站在布景幕的位置,谁就能被显示在照相机里。每添加一个人,记录总数的pageNode就加1。
跳转的概念其实就是让这些排队照相的人向前或向后移动到指定的位置,所以这里我们只需要让整个队伍(也就是LevelLayer层)runAction就能实现移动。

在LevelLayer中最重要的是我们必须实现触摸监听,所以下面来一起看看其触摸回调函数如何实现吧。

bool LevelLayer::onTouchBegan(Touch *touch, Event  *event)
{
    touchDownPoint = touch->getLocation();
    touchCurPoint = touchDownPoint;
    return true;
}

void LevelLayer::onTouchMoved(Touch *touch, Event  *event)
{
    Point touchPoint = touch->getLocation();
    auto currX = this->getPositionX() + touchPoint.x - touchCurPoint.x;
    Point posPoint = Point::Point(currX, getPositionY());
    auto dis= fabsf(touchPoint.x - touchCurPoint.x);
    if (dis >= SHORTEST_SLIDE_LENGTH ) {
        this->setPosition(posPoint);
    }
    touchCurPoint = touchPoint;

}

void LevelLayer::onTouchEnded(Touch *touch, Event  *event)
{
    touchUpPoint = touch->getLocation();
    auto dis= touchUpPoint.getDistance(touchDownPoint);
    auto sprite1 =Sprite::createWithSpriteFrameName("page_mark1.png");
    auto width = sprite1->getContentSize().width;
    if (dis >= SHORTEST_SLIDE_LENGTH )
    {
        int offset = getPositionX() - curPageNode * (-WINDOW_WIDTH);
        if (offset > width) {
            if (curPageNode > 0) {
                --curPageNode;
                Sprite *sprite =  (Sprite *)LevelScene::getInstance()->getChildByTag(888);
                sprite->setPosition(Point(sprite->getPositionX()-width,sprite->getPositionY()));
            }
        }
        else if (offset < -width) {
            if (curPageNode < (pageNode - 1)) {
                ++curPageNode;
                Sprite *sprite =  (Sprite *)LevelScene::getInstance()->getChildByTag(888);
                sprite->setPosition(Point(sprite->getPositionX()+width,sprite->getPositionY()));
            }
        }
        goToCurrNode();
    }
}

在开始触摸屏幕时,记录下触摸按下点和当前触摸点;在屏幕上移动时,记录下当前的触摸点并计算滑动的距离,如果滑动的距离超过我们给出的最短滑动长度,那么我们就设置LevelLayer的位置到移动的地方;在结束触摸时,同样记录抬起的点并计算按下点与抬起点的之间的距离,当这一距离大于给定的SHORTEST_SLIDE_LENGTH时,根据具体情况跳转到其他位置,同时设置游动标记的位置。
result2

子选项

本游戏中一共有三页关卡选择项,在每页选择项上又有六个小的按钮。这里玩家选择哪个按钮,就会进入哪个关卡。因为每页选择项的背景图片,以及它的子按钮所代表的关卡数都是不一样的,所有我们需要充分的考虑到这些因素,抽象出一个高耦合的类模块来实现它。
接下来我们将来看看这个关卡选择页面(LevelSelectPage)的实现方法,其继承于Node,下面是其定义:

class LevelSelectPage: public Node
{
public:

    bool initLevelPage(const std::string& bgName, int level);
    static LevelSelectPage* create(const std::string& bgName, int level);
    void menuStartCallback(Ref* pSender);
};

LevelSelectPage类很简单,create是创建LevelSelectPage对象的静态方法,而initLevelPage方法则会初始化LevelSelectPage对象的背景,菜单等属性。
当玩家点了LevelSelectPage上的按钮时,将会触发menuStartCallback按钮回调函数。

我们先来看看LevelSelectPage的初始化,代码如下:

bool LevelSelectPage::initLevelPage(const std::string& bgName, int level)
{
    if (!Node::init())
    {
        return false;
    }
    // 1
    auto size = Director::getInstance()->getWinSize();
    auto sprite =Sprite::create(bgName);
    sprite->setPosition(Point(size.width / 2,size.height / 2));
    addChild(sprite,-2);
    // 2
    Vector menuItemVector;
    auto buttonSize = Sprite::create("card_1.png")->getContentSize();
    auto gap = buttonSize.width / 4;
    auto startWidth =( size.width -  LEVEL_ROW* buttonSize.width - (LEVEL_ROW - 1)*gap ) /2 ;
    auto startHeight = ( size.height + (LEVEL_COL - 1) * gap + buttonSize.height * LEVEL_COL ) /2 - buttonSize.height;
    for ( int row = 0; row < LEVEL_COL; row++ )
    {
        auto height = startHeight - ( buttonSize.height + gap ) * row ;
        for ( int col = 0; col < LEVEL_ROW; col++)
        {
            auto width = startWidth + ( buttonSize.width + gap ) * col ;
            auto item = MenuItemImage::create(
                                              "card_1.png",
                                              "card_2.png",
                                              "card_3.png",
                                              CC_CALLBACK_1(LevelSelectPage::menuStartCallback, this));
            item->setAnchorPoint(Point(0 ,0));
            item->setPosition(Point(width, height));
            item->setTag(row * LEVEL_ROW + col + level * LEVEL_ROW * LEVEL_COL);
            menuItemVector.pushBack( item );
            auto levelNum = UserDefault::getInstance()->getIntegerForKey("levelNum");

            if(levelNum < row * LEVEL_ROW + col + level * LEVEL_ROW * LEVEL_COL)
            {
                item->setEnabled(false);
            }
        }
    }
    auto levelMenu = Menu::createWithArray(menuItemVector);
    levelMenu->setPosition(Point::ZERO);
    this->addChild(levelMenu, -1);

    return true;
}
  1. 根据参数给的图片名,创建选项页的背景。
  2. 创建一组按钮项,依次排开。这里需要注意的是,我们为每个按钮都设置了一个标记,通过这个标记我们可以知道玩家选择的是哪个关卡。

在按钮回调函数中我们会获取玩家所选择按钮的标记值,再根据该值读取响应的关卡数据。代码如下:

void LevelSelectPage::menuStartCallback(Ref* pSender)
{
    auto button = (Sprite *)pSender;
    SimpleAudioEngine::getInstance()->playEffect(FileUtils::getInstance()->fullPathForFilename("sound/button.wav").c_str(), false);

    char buffer[20] = { 0 };
    sprintf(buffer, "levelInfo_%d.plist", button->getTag());
    std::string strName =  buffer;
    UserDefault::getInstance()->setStringForKey("nextLevelFile", strName);

    Scene *scene = Scene::create();
    auto layer = LevelInfoScene::create();
    scene->addChild(layer);
    auto sence = TransitionFade::create(0.5, scene);
    Director::getInstance()->replaceScene(sence);
}

下面分别是第0关和第1关的关卡:

游戏中还有一点要提的是,我们通过UserDefault数据来判断游戏解锁关卡数、游戏当前要进入关卡的关卡名,以及已通关关卡的分数(星星数)。所有在程序中大家会看到在很多地方都在set键值,get键值,这里就不一一解释了,看到程序应该都能明白。

OK,现在我们就可以创建一个场景(LevelScene),把LevelLayer层和这些子选项按层级依次加到里面了,详见代码。

当然为了美观,我们在场景中加入了如同下雪一样的粒子特效,关于该效果的实现可阅读使用Cocos2d-x和粒子编辑器实现“天天爱消除”场景特效一文。

标签: cocos2d-x教程

?>