用Cocos2d-x v3.0做一個貪喫蛇遊戲

寫在前面的廢話:

在CocoaChina上看到了“進擊的貪喫蛇”這個活動,翻了一下論壇也沒什麼人真的把教程發出來,正好今天閒的沒事就寫寫這東西吧。自己之前一直在用2.0版本的cocos2dx,對於3.0幾次想開坑學習都因爲各種原因放棄掉了,版本更新太快,C++11新特性不熟等等。今天花了一天時間把這個東西搞定,才發現前面那些基本只是藉口,雖然C++11的新特性啥的依舊不是太會,不過也能照貓畫虎用一用v3.0了,只要努力去做,肯定有些收穫的。

自己本身就是個不喜歡囉嗦的人,之前也想寫過博客什麼的,結果寫完之後發現寥寥幾句似乎就沒得說了,表達能力果然還需要磨練。廢話不多說,爲了讓我這東西能讓更多的人看懂,不妨就把目標羣體定位在用過v2.0的卻感覺對v3.0無從下手的人吧,因此並不會解釋很多基礎的東西。通過這個教程,應該就會發現3.0也沒那麼可怕。


創建工程:

我現在用的最新版v3.0rc1的創建方式,和以前v2.2的python腳本創建方式又有了些區別,改爲了通過cocos-console來創建新工程。

具體的步驟可以參考子龍山人的教學視頻,這裏就不再贅述了。

視頻地址:http://v.youku.com/v_show/id_XNjg1Mzc4ODQ0.html


創建開始:

創建開始界面我們並不新建文件,直接修改HelloWorldScene.h和.cpp。

開始界面有以下幾個元素:"Start Game","Game Help", “Exit Game"的文字按鈕,還有本身HelloWorld自帶的那些東西(我刪掉了關閉的按鈕)。

因此我們要做的只是在原來代碼的基礎上添加我們的文字按鈕以及設置回調函數就好。


看代碼之前我們來認識v3.0和v2.0的區別之一,也就是類型名稱的改動。其實很簡單,只是以前帶CC的東西全部去掉就好CCLayer 變成 Layer,CCSprite 變成 Sprite,僅此而已,當然也有些特例。感興趣的可以看下:引擎中CCType.h裏面寫的東西。

我們來看下HelloWorldScene.h的代碼:

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"

USING_NS_CC;

class HelloWorld : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();
    CREATE_FUNC(HelloWorld);
    void menuCloseCallback(Ref* pSender);
};

#endif // __HELLOWORLD_SCENE_H__

並沒有修改任何代碼,只是刪掉了註釋,沒什麼可多說的。不過我們有兩點可以注意到:

1.按鈕的回調函數的參數類型從以前的CCobject* 變成了 Ref*。

一般來講,從v2.0到v3.0,類型名稱的變化基本都是把開頭的CC去掉即可。而CCobject算是個特例吧,直接變成了Ref。爲了確認我特意試了下,並沒有Object這麼個類型。

2.創建scene的函數名稱從scene變成了createScene。

然後是HelloWorldScene.cpp的代碼:

USING_NS_CC;

Scene* HelloWorld::createScene()
{
    auto scene = Scene::create();
    auto layer = HelloWorld::create();
    scene->addChild(layer);
    return scene;
}

createScene函數和以前比沒任何變化,還是那四句。

然後我們編寫init函數。刪掉原來的註釋,在Point origin = Director::getInstance()->getVisibleOrigin();這句下面開始添加自己的代碼

    //創建文字按鈕
    auto labelStart = Label::create("Start Game", "宋體", 24);
    auto labelHelp = Label::create("Game Help", "宋體", 24);
    auto labelExit = Label::create("Exit Game", "宋體", 24);
    
    auto uiStart = MenuItemLabel::create(labelStart, CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
    uiStart->setTag(1);
    uiStart->setPosition(Point(100,200));
    
    auto uiHelp = MenuItemLabel::create(labelHelp, CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
    uiHelp->setTag(2);
    uiHelp->setPosition(Point(100,150));
    
    auto uiExit = MenuItemLabel::create(labelExit, CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
    uiExit->setTag(3);
    uiExit->setPosition(Point(100,50));
    
    auto menu = Menu::create(uiStart,uiHelp, uiExit, NULL);
    menu->setPosition(Point::ZERO);
    this->addChild(menu, 1);

我們可以看到,創建一個文本構成的ui菜單還是和以前一樣,先創建文字標籤,再創建對應的MenuItemLabel,最後創建Menu把MenuItemLabel都加進去,最後在讓Layer添加Menu完事。

過程仍然一樣,其中也有些不可忽視的東西:

1.auto

我們可以看到,所有的變量創建的時候都設爲了auto類型(這說法不太準確不要在意)。auto關鍵字用於在創建變量的時候自動判斷類型,讓編程更方便一些。當然也可以照我們以前寫的Label labelStart這樣創建變量,不過要寫的東西多了點就是了。

關於auto更深入的東西,可以參考這篇文章:點擊打開鏈接

2.Label

我們之前一直用的是CCLabelTTF,理論上我們在此要使用LabelTTF,但是我在這裏使用了Label。並非labelTTF被刪除,“Hello World”這個文本仍然是由LabelTTF創建,可見他還存在並且可用。Label是v3.0中出現的新類型,它比以前LabelTTF更快,並且還有陰影,描邊等效果。在這裏我並沒有用到什麼label的特殊效果,不過就憑更快這點,我們也可以拿Label來替換掉所有的LabelTTF了。

關於Label的學習,可以直接參考官方文檔:http://www.cocos2d-x.org/docs/manual/framework/native/gui/label/v3/en

3。Point

CCPoint變成了Point這樣理所當然,不過很多東西也是有了些改變。比如我們平時用的ccp(x,y)這個函數被取消,直接用構造函數來創建新的點。同時ccpAdd等運算的函數也被取消掉,通過+ - * /等運算符來替代。CC_POINT_ZERO也變成了Point::Zero。

  最後是回調函數:

void HelloWorld::menuCloseCallback(Ref* pSender)
{
    int i = dynamic_cast<Node*>(pSender)->getTag();
    switch (i)
    {
        case 1:
            log("go to Game");
            //Director::getInstance()->replaceScene(GameLayer::createScene());
            break;
        case 2:
            log("go to Help");
            //Director::getInstance()->replaceScene(GameHelp::createScene());
            break;
        case 3:
        case 4:
        {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
            MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert");
            return;
#endif
            
            Director::getInstance()->end();
            
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
            exit(0);
#endif
        }
            break;
        default:
            break;
    }
    
}

在這裏我們並沒有創建多個回調函數,而是在一個函數裏通過判斷之前給各個按鈕添加的Tag從而執行不同的功能。

寫好這些代碼後,build工程,點擊各個文字按鈕是否有效。幫助界面和遊戲界面我們還沒有寫,所以只會出現調試信息 go to game 和go to help。


創建幫助界面:


幫助界面元素:返回主菜單的文字按鈕,中央的說明文字。

幫助界面並沒有什麼新東西,寫這個部分的代碼就權當複習上面的東西了。

GameHelp.h:

#ifndef __Snake__GameHelp__
#define __Snake__GameHelp__

#include <iostream>
#include "cocos2d.h"

class GameHelp : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();
    CREATE_FUNC(GameHelp);
    void menuBackToMain(cocos2d::Ref *pSender);
};

#endif /* defined(__Snake__GameHelp__) */

除了類型的名稱和回調函數名不一樣,其餘均和HelloWorldScene.h中的一致。


GameHelp.cpp

#include "GameHelp.h"
#include "HelloWorldScene.h"

USING_NS_CC;

Scene* GameHelp::createScene()
{
    auto scene = Scene::create();
    auto layer = GameHelp::create();
    scene->addChild(layer);
    return scene;
}

bool GameHelp::init()
{
    if(!Layer::init())
    {
        return false;
    }
    
    //創建幫助文字
    auto labelHelp = Label::create("Please click screen to start the game!", "宋體", 24);
    labelHelp->setPosition(Point(480,320));
    this->addChild(labelHelp);
    
    //創建返回按鈕
    auto labelBack = Label::create("MainMenu", "宋體", 24);
    auto uiBack = MenuItemLabel::create(labelBack, CC_CALLBACK_1(GameHelp::menuBackToMain, this));
    uiBack->setPosition(Point(100,50));
    
    auto menu = Menu::create(uiBack,NULL);
    menu->setPosition(Point::ZERO);
    this->addChild(menu);
    
    return true;
    
}

void GameHelp::menuBackToMain(cocos2d::Ref *pSender)
{
    Director::getInstance()->replaceScene(HelloWorld::createScene());
}

創建兩個元素的方式也和之前一樣,沒有什麼可太多說明的。需要注意的是回調函數這裏,我們之前慣用的CCDirector::sharedDirector變成了getInstance,除了名稱有變化,功能還是一樣的。而且,在v2.2.3裏面也變成了這樣子。

寫完代碼之後,在HelloWorldScene.cpp中包含GameHelp.h並且把回調函數中case2 的註釋變成代碼。build後看是否能進行界面的順利跳轉。


創建遊戲界面:


遊戲界面的元素:蛇的身體、蛋、網格

終於到了重頭戲。這個貪喫蛇遊戲比較簡單,只有蛇和蛋還有網格,沒有提供記分系統。機制上也並沒有常見的加速,撞牆,咬到自己也不會死,可以說是去掉了很多機制的極簡版貪喫蛇。感興趣的讀者可以自己加上各種機制,也並不太難,就看大家的精力了。

我們先看GameLayer.h:

enum DIR_DEF
{
    UP = 1,
    DOWN,
    LEFT,
    RIGHT
};
class SnakeNode: public cocos2d::Ref
{

public:
    DIR_DEF dir;
    int row, col;
};

class GameLayer: public cocos2d::Layer
{
private:
    
    SnakeNode* m_head;
    SnakeNode* m_food;
    cocos2d::Vector<SnakeNode*> m_body;
    
public:
    virtual bool init();
    virtual void draw(cocos2d::Renderer *renderer, const kmMat4 &transform, bool transformUpdated);
    void update(float dt);
    static cocos2d::Scene* createScene();
    CREATE_FUNC(GameLayer);
    void menuBackToMain(cocos2d::Ref *pSender);
    
    virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);
    
};
在這裏我們多定義了一個類SnakeNode,用來表示畫面中的元素(蛇頭、蛇的身體、還有蛋都算),因爲只有變量,並沒有方法和實現,所以沒有多建立一個文件。

GameLayer中的大部分和之前的都差不多,值得注意的有兩點:

1.draw函數

因爲我們並沒有提供任何額外的圖片,所以我們所有的繪製都要重載draw函數從而調用基本的繪圖函數來繪製遊戲場景。在之前我們一直用virtual void draw()來重載draw函數,但是在3.0裏沒有任何參數的draw函數被設爲了final,即無法重載的,取而代之的是代碼中的virtual void draw(Renderer* renderer, const kmMat4 &transform, bool transformUpdated),多了很多參數,但是用法其實還是和以前一樣。我們只需要把我們的繪製函數寫進去,最後再

調用父類的draw函數即可。


2.Vector

在v3.0中已經不推薦使用我們常用的CCArray,取而代之的是Vector,類似於我們在C++中經常用的std::Vector。在我看來最大的優勢在於聲明瞭元素類型,從而不用把每個取出來的元素都進行類型轉換,方便了很多也安全了很多。


    GameLayer.cpp

#include "GameLayer.h"
#include "HelloWorldScene.h"

USING_NS_CC;

Scene* GameLayer::createScene()
{
    auto scene = Scene::create();
    auto layer = GameLayer::create();
    scene->addChild(layer);
    return scene;
}

bool GameLayer::init()
{
    if(!Layer::init())
    {
        return false;
    } 
    //創建返回按鈕
    auto labelBack = Label::create("MainMenu", "宋體", 24);
    auto uiBack = MenuItemLabel::create(labelBack, CC_CALLBACK_1(GameLayer::menuBackToMain, this));
    uiBack->setPosition(Point(900,200));
    
    auto menu = Menu::create(uiBack, NULL);
    menu->setPosition(Point::ZERO);
    this->addChild(menu);
    //隨機初始化蛇以及蛋
    m_head = new SnakeNode();
    m_head->row = arc4random()%10;
    m_head->col = arc4random()%10;

    m_food = new SnakeNode();
    m_food->row = arc4random()%10;
    m_head->col = arc4random()%10;
    //建立觸摸監聽
    auto listener1 = EventListenerTouchOneByOne::create();
    listener1->setSwallowTouches(true);
    listener1->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan, this);
    
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, this);
    
    this->schedule(schedule_selector(GameLayer::update), 0.5);
    
    return true;
}

createScene和之前一樣不再贅述。在Init函數中我們可以看到,Vector並不需要像CCArray那樣進行create,只需要創建就好。另外,我們看到了一個新的東西Listener,這個是v3.0中新的觸摸機制,取代了之前的setTouchEnabled等等觸摸機制。

新的觸摸機制:

新的觸摸機制採取了類似於flash中的listener,每次發生相應的時間就執行對應的函數,它不光可以像以前那樣掛在Layer上,也可以掛在Sprite等等東西上。這樣就不用再有滿天飛的如何寫一個可接受觸摸的Sprite類等等的問題了。

實現新的觸摸機制我們需要以下幾步:

1.創建新的EventListenner,EventListener不光是接受觸摸的,鍵盤什麼的也改成了這種機制,不過這裏我們只討論觸摸。

auto listener1 = EventListenerTouchOneByOne::create();

auto listener2 = EventListnereTouchAllatOnce::create();

  這兩種類型分別對應了單點觸摸和多點觸摸。相比以前的多點觸摸用setTouchEnabled,單點用觸摸代理註冊的混亂情形有了很大改善。

2.設置觸摸的回調函數。

  這個和以前基本一樣,還是分爲了began,moved,ended和canceled四種,只不過我們需要手動選擇在這些事件發生時他們要加載的函數

listener1->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan,this);

  一般的教程都直接把函數寫在了等號後面並沒有新建函數,這裏給出了已經寫出函數後調用的方法。

3.掛載監聽

  _eventDispatcher->addEventListenerWithSceneGraphPriority(listener1,this);

  _eventDispatcher是節點自帶的觸摸管理器,通過addEventListenerWithSceneGraphPriority可以向對象上掛載監聽,第二個參數可以是某個Sprite甚至是label等等。觸摸的優先級則是根據圖片顯示的順序而來,越在上面的優先級越高。如果之前設置了SwallowTouches爲true那麼會吞噬掉優先度低的觸摸。


關於進一步的觸摸機制說明,參見官方文檔:http://www.cocos2d-x.org/docs/manual/framework/native/input/event-dispatcher/en


讓我們接着看下面的代碼:

void GameLayer::draw(cocos2d::Renderer *renderer, const kmMat4 &transform, bool transformUpdated)
{
    //繪製直線
    glLineWidth(4);
    for (int i = 0; i<11; i++)
    {
        DrawPrimitives::drawLine(Point(0,i*64), Point(640,i*64));
        DrawPrimitives::drawLine(Point(i*64,0), Point(i*64,640));
    }
    
    //繪製蛇頭
    DrawPrimitives::drawSolidRect(Point(m_head->col * 64 , m_head->row * 64 ),
                                  Point(m_head->col * 64 +64,m_head->row * 64 +64),
                                  Color4F(1.0f,0,0,1.0f));
    
    //繪製蛋
    DrawPrimitives::drawSolidRect(Point(m_food->col * 64 , m_food->row * 64 ),
                                  Point(m_food->col * 64 +64,m_food->row * 64 +64),
                                  Color4F(0,0,1.0f,1.0f));
    
    //繪製蛇身
    for(auto &sn : m_body)
    {
        DrawPrimitives::drawSolidRect(Point(sn->col * 64, sn->row * 64),
                                      Point(sn->col * 64 + 64, sn->row * 64 +64),
                                      Color4F(0,0,1.0f,1.0f));
    }
    
    Layer::draw(renderer, transform, transformUpdated);
}

void GameLayer::update(float dt)
{
    //蛇身每一段跟隨前一段移動
    for(int i = m_body.size() -1 ;i>=0;i--)
    {
        SnakeNode* sn = m_body.at(i);
        
        if(i!=0)
        {
            SnakeNode* pre = m_body.at(i-1);
            sn->dir = pre->dir;
            sn->col = pre->col;
            sn->row = pre->row;
        }
        else
        {
            sn->dir = m_head->dir;
            sn->col = m_head->col;
            sn->row = m_head->row;
        }
    }
    
    //根據方向來讓蛇頭移動
    switch (m_head->dir)
    {
        case UP:
            m_head->row++;
            log("up");
            if(m_head->row >=10) m_head->row = 0;
            break;
        case DOWN:
            m_head->row--;
            log("down");
            if(m_head->row <0) m_head->row = 9;
            break;
        case RIGHT:
            m_head->col++;
            if(m_head->col >=10) m_head->col = 0;
            break;
        case LEFT:
            m_head->col--;
            if(m_head->col < 0) m_head->col = 9;
            break;
        default:
            break;
    }
    
    //和蛋的碰撞檢測
    if(m_head->row == m_food->row && m_head->col == m_food->col)
    {
        //刷新蛋
        m_food->row = arc4random()%10;
        m_food->col = arc4random()%10;
        
        //設置新的尾巴的參數
        SnakeNode* sn = new SnakeNode();
        SnakeNode* last = NULL;
        if(m_body.size() > 0)
        {
            last = m_body.back();
        }
        else
        {
            last = m_head;
        }
        
        switch (last->dir)
        {
            case UP:
                sn->row = last->row - 1;
                sn->col = last->col;
                break;
            case DOWN:
                sn->row = last->row + 1;
                sn->col = last->col;
                break;
            case LEFT:
                sn->row = last->row;
                sn->col = last->col + 1;
                break;
            case RIGHT:
                sn->row = last->row;
                sn->col = last->col - 1;
                break;
                
            default:
                break;
        }
        //添加進身體的Vector中
        m_body.pushBack(sn);
    }
}

void GameLayer::menuBackToMain(cocos2d::Ref *pSender)
{
    Director::getInstance()->replaceScene(HelloWorld::createScene());
}

bool GameLayer::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event)
{
    Point tPos = touch->getLocation();
    int nowRow = ((int)tPos.y)/64;
    int nowCol = ((int)tPos.x)/64;
    
    if(abs(nowRow - m_head->row) > abs(nowCol - m_head->col))
    {
        //上下移動
        if(nowRow > m_head->row)
        {
            m_head->dir  = UP;
        }
        else
        {
            m_head->dir  = DOWN;
        }
    }
    else
    {
        //左右移動
        if(nowCol > m_head->col)
        {
            m_head->dir = RIGHT;
        }
        else
        {
            m_head->dir = LEFT;
        }
    }
    
    return true;
}

代碼很長,我們一一來看。

首先是menu的回調函數,和之前一樣,沒什麼好說的。

接下來是觸摸的回調函數onTouchBegan,依舊是touch->getLocation(),之後再去寫邏輯這樣的用法,可以說和以前一模一樣,完全無須擔心什麼。

再來看draw函數,多了一些參數其實並沒有用上。基本的繪製函數還是和以前一樣,只不過每個函數之前都要加上DrawPrimitives::這個命名空間了,不知道是不是我xcode的問題,不加這個它就報錯。

最後我們來看update函數。算法很清晰,我覺得做過一兩個遊戲的人都能簡單的看懂。我們來簡要的說些Vector的用法,Vector基本相當於std::Vecot所以之前CCArray那種和NSArray風格的函數都改頭換面了。addObject變爲pushBack,objectAtIndex變爲at,lastObject變爲back,count變爲size。基本上換湯不換藥,還多了一些find,insert等等的功能。以前需要retain,現在也沒什麼必要了。

不過Vector也並沒有那麼簡單,內存的申請什麼的參考官方文檔:http://www.cocos2d-x.org/docs/manual/framework/native/data-structure/v3/vector/en

關於各種ptr什麼的我理解並沒有那麼清晰,所以在代碼中也沒有這麼做,關於這方面還請各位大大指教。


寫在最後:

文章寫到這裏就差不多了,相信諸位也和我一樣初步理解了v3.0基礎的一些東西,可以用v3.0做一些小東西出來了。整體文章對貪喫蛇的算法並沒有任何的教學,因爲我認爲有過一些經驗的人這並不是什麼難事,而且直接看代碼也可以看懂。文章的重點還是在於從2.0怎麼開始學習3.0上,因此多了很多的參考資料,更像是一個各類文章的不完全集合貼,希望給之前和我一樣迷茫的人一些幫助吧。第一次寫這麼長的教程,可能並沒有很多人看得懂,還請各位海涵。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章