1. 程式人生 > >如何製作一個類似Tiny Wings的遊戲 Cocos2d-x 2 1 4

如何製作一個類似Tiny Wings的遊戲 Cocos2d-x 2 1 4

                步驟如下:1.使用上一篇的工程;2.新增地形類Terrain,派生自CCNode類。檔案Terrain.h程式碼如下:
123456789101112131415161718#pragma once#include "cocos2d.h"#define kMaxHillKeyPoints 1000class Terrain : public cocos2d::CCNode{public:    Terrain(void);    ~Terrain(void);    CREATE_FUNC(Terrain);    CC_SYNTHESIZE_RETAIN(cocos2d::CCSprite*, _stripes, Stripes);private
:    int _offsetX;    cocos2d::CCPoint _hillKeyPoints[kMaxHillKeyPoints];};

這裡聲明瞭一個_hillKeyPoints陣列,用來儲存每個山丘頂峰的點,同時聲明瞭一個_offsetX代表當前地形滾動的偏移量。檔案Terrain.cpp程式碼如下: 

123456789101112#include "Terrain.h"using namespace cocos2d;Terrain::Terrain(void){    _stripes = NULL;    _offsetX = 0;}Terrain::~Terrain(void
){    CC_SAFE_RELEASE_NULL(_stripes);}

增加如下方法: 

123456789101112void Terrain::generateHills(){    CCSize winSize = CCDirector::sharedDirector()->getWinSize();    float x = 0;    float y = winSize.height / 2;    for (int i = 0; i < kMaxHillKeyPoints; ++i)    {        _hillKeyPoints[i] = ccp(x, y);        x += winSize.width / 2
;        y = rand() % (int)winSize.height;    }}

這個方法用來生成隨機的山丘頂峰的點。第一個點在螢幕的左側中間,之後的每一個點,x軸方向移動半個螢幕寬度,y軸方向設定為0到螢幕高度之間的一個隨機值。新增以下方法: 

1234567891011121314151617181920212223bool Terrain::init(){    bool bRet = false;    do     {        CC_BREAK_IF(!CCNode::init());        this->generateHills();        bRet = true;    } while (0);    return bRet;}void Terrain::draw(){    CCNode::draw();    for (int i = 1; i < kMaxHillKeyPoints; ++i)    {        ccDrawLine(_hillKeyPoints[i - 1], _hillKeyPoints[i]);    }}

init方法呼叫generateHills方法建立山丘,draw方法簡單地繪製相鄰點之間的線段,方便視覺化除錯。新增以下方法: 

12345void Terrain::setOffsetX(float newOffsetX){    _offsetX = newOffsetX;    this->setPosition(ccp(-_offsetX * this->getScale(), 0));}

英雄沿著地形的x軸方法前進,地形向左滑動。因此,偏移量需要乘以-1,還有縮放比例。開啟HelloWorldScene.h檔案,新增標頭檔案引用: 

1#include "Terrain.h"

新增如下變數: 

1Terrain *_terrain;

開啟HelloWorldScene.cpp檔案,在onEnter方法裡,呼叫genBackground方法之前,加入如下程式碼: 

12_terrain = Terrain::create();this->addChild(_terrain, 1);

update方法裡,最後面新增如下程式碼: 

1_terrain->setOffsetX(offset);

修改genBackground方法為如下: 

123456789101112131415161718192021222324void HelloWorld::genBackground(){    if (_background)    {        _background->removeFromParentAndCleanup(true);    }    ccColor4F bgColor = this->randomBrightColor();    _background = this->spriteWithColor(bgColor, 512512);     CCSize winSize = CCDirector::sharedDirector()->getWinSize();    _background->setPosition(ccp(winSize.width / 2, winSize.height / 2));    ccTexParams tp = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT};    _background->getTexture()->setTexParameters(&tp);    this->addChild(_background);    ccColor4F color3 = this->randomBrightColor();    ccColor4F color4 = this->randomBrightColor();    CCSprite *stripes = this->spriteWithColor1(color3, color4, 5125124);    ccTexParams tp2 = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_CLAMP_TO_EDGE};    stripes->getTexture()->setTexParameters(&tp2);    _terrain->setStripes(stripes);}

注意,每次觸控式螢幕幕,地形上的條紋紋理都會隨機生成一個新的條紋紋理,這方便於測試。此外,在Update方法裡_background呼叫setTextureRect方法時,可以將offset乘以0.7,這樣背景就會比地形滾動地慢一些。編譯執行,可以看到一些線段,連線著山丘頂峰的點,如下圖所示:當看到山丘滾動,可以想象得到,這對於一個Tiny Wings遊戲,並不能很好的工作。由於採用y軸隨機值,有時候山丘太高,有時候山丘又太低,而且x軸也沒有足夠的差別。但是現在已經有了這些測試程式碼,是時候用更好的演算法了。3.更好的山丘演算法。使用Sergey的演算法來進行實現。開啟Terrain.cpp檔案,修改generateHills方法為如下: 

123456789101112131415161718192021222324252627282930313233343536373839404142void Terrain::generateHills(){    CCSize winSize = CCDirector::sharedDirector()->getWinSize();    float minDX = 160;    float minDY = 60;    int rangeDX = 80;    int rangeDY = 40;    float x = -minDX;    float y = winSize.height / 2;    float dy, ny;    float sign = 1// +1 - going up, -1 - going  down    float paddingTop = 20;    float paddingBottom = 20;    for (int i = 0; i < kMaxHillKeyPoints; ++i)    {        _hillKeyPoints[i] = ccp(x, y);        if (i == 0)        {            x = 0;            y = winSize.height / 2;        }         else        {            x += rand() % rangeDX + minDX;            while (true)            {                dy = rand() % rangeDY + minDY;                ny = y + dy * sign;                if (ny < winSize.height - paddingTop && ny > paddingBottom)                {                    break;                }            }            y = ny;        }        sign *= -1;    }}

這個演算法執行的策略如下:

  • 在範圍160加上0-80之間的隨機數進行遞增x軸。
  • 在範圍60加上0-40之間的隨機數進行遞增y軸。
  • 每次都反轉y軸偏移量。
  • 不要讓y軸值過於接近頂部或底部(paddingTop, paddingBottom)。
  • 開始於螢幕外的左側,硬編碼第二個點為(0, winSize.height/2),所以左側螢幕外有一個山丘。
編譯執行,現在可以看到一個更好的山丘演算法,如下圖所示:4.一次只繪製部分。在更進一步之前,需要做出一項重大的效能優化。現在,繪製出了山丘的1000個頂峰點,即使每次都只有少數在螢幕上看得到。所以,可以根據螢幕區域來計算哪些頂峰點會被顯示出來,然後只顯示那些點,如下圖所示:開啟Terrain.h檔案,新增如下變數: 
12int _fromKeyPointI;int _toKeyPointI;

開啟Terrain.cpp檔案,在建構函式裡面新增如下程式碼: 

12_fromKeyPointI = 0;_toKeyPointI = 0;

新增如下方法: 

1234567891011121314151617void Terrain::resetHillVertices(){    CCSize winSize = CCDirector::sharedDirector()->getWinSize();    static int prevFromKeyPointI = -1;    static int prevToKeyPointI = -1;    // key points interval for drawing    while (_hillKeyPoints[_fromKeyPointI + 1].x < _offsetX - winSize.width / 8 / this->getScale())    {        _fromKeyPointI++;    }    while (_hillKeyPoints[_toKeyPointI].x < _offsetX + winSize.width * 9 / 8 / this->getScale())    {        _toKeyPointI++;    }}

這裡,遍歷每一個頂峰點(從0開始),將它們的x軸值拿來做比較。無論當前對應到螢幕左邊緣的偏移量設定為多少,只要將它減去winSize.width/8。如果頂峰點的x軸值小於結果值,那麼就繼續遍歷,直到找到一個大於結果值的,這個頂峰點就是顯示的起始點。對於toKeypoint也採用同樣的過程。修改draw方法,程式碼如下: 

123456789void Terrain::draw(){    CCNode::draw();    for (int i = MAX(_fromKeyPointI, 1); i <= _toKeyPointI; ++i)    {        ccDrawColor4F(1.0001.0);        ccDrawLine(_hillKeyPoints[i - 1], _hillKeyPoints[i]);    }}

現在,不是繪製所有點,而是隻繪製當前可見的點,這些點是前面計算得到的。另外,也把線的顏色改成紅色,這樣更易於分辨。接著,在init方法裡面,最後面新增如下程式碼: 

1this->resetHillVertices();

setOffsetX方法裡面,最後面新增如下程式碼: 

1this->resetHillVertices();

為了更容易看到,開啟HelloWorldScene.cpp檔案,在onEnter方法,最後面新增如下程式碼:

1this->setScale(0.25);

編譯執行,可以看到線段出現時才進行繪製,如下圖所示:5.製作平滑的斜坡。山丘是有斜坡的,而不是這樣直上直下的直線。一個辦法是使用餘弦函式讓山丘彎曲。回想一下,餘弦曲線就如下圖所示:因此,它是從1開始,每隔PI長度,曲線下降到-1。但怎麼利用這個函式來建立一個漂亮的曲線連線頂峰點呢?先只考慮兩個點的情況,如下圖所示:首先,需要分段繪製線,因此,需要每10個點建立一個區段。同樣的,想要一個完整的餘弦曲線,因此,可以將PI除以區段的數量,得到每個點的角度。然後,讓cos(0)對應p0的y軸值,而cos(PI)對應p1的y軸值。要做到這一點,將呼叫cos(angle),乘以p1和p0之間距離的一半(圖上的ampl)。由於cos(0)=1,而cos(PI)=-1,所以,ampl在p0,而-ampl在p1。將它加上中點座標,就可以得到想要的y軸值。開啟Terrain.h檔案,新增區段長度定義,如下程式碼: 

1#define kHillSegmentWidth 10

然後,開啟Terrain.cpp檔案,在draw方法裡面,ccDrawLine之後,新增如下程式碼: 

1234567891011121314151617181920ccDrawColor4F(1.01.01.01.0);CCPoint p0 = _hillKeyPoints[i - 1];CCPoint p1 = _hillKeyPoints[i];int hSegments = floorf((p1.x - p0.x) / kHillSegmentWidth);float dx = (p1.x - p0.x) / hSegments;float da = M_PI / hSegments;float ymid = (p0.y + p1.y) / 2;float ampl = (p0.y - p1.y) / 2;CCPoint pt0, pt1;pt0 = p0;for (int j = 0; j < hSegments + 1; ++j){    pt1.x = p0.x + j * dx;    pt1.y = ymid + ampl * cosf(da * j);    ccDrawLine(pt0, pt1);    pt0 = pt1;}

開啟HelloWorldScene.cpp檔案,在onEnter方法,設定scale為1.0,如下程式碼: 

1this->setScale(1.0);

編譯執行,現在可以看到一條曲線連線著山丘,如下圖所示:6.繪製山丘。用上一篇文章生成的條紋紋理來繪製山丘。計劃是對山丘的每個區段,計算出兩個三角形來渲染山丘,如下圖所示:還將設定每個點的紋理座標。對於x座標,簡單地除以紋理的寬度(因為紋理重複)。對於y座標,將山丘的底部對映為0,頂部對映為1,沿著條帶的方向分發紋理高度。開啟Terrain.h檔案,新增如下程式碼: 

12#define kMaxHillVertices 4000#define kMaxBorderVertices 800

新增類變數,程式碼如下: 

12345int _nHillVertices;cocos2d::CCPoint _hillVertices[kMaxHillVertices];cocos2d::CCPoint _hillTexCoords[kMaxHillVertices];int _nBorderVertices;cocos2d::CCPoint _borderVertices[kMaxBorderVertices];

開啟Terrain.cpp檔案,在resetHillVertices方法裡面,最後面新增如下程式碼: 

1234567891011121314151617181920212223242526272829303132333435363738394041424344if (prevFromKeyPointI != _fromKeyPointI || prevToKeyPointI != _toKeyPointI){    // vertices for visible area    _nHillVertices = 0;    _nBorderVertices = 0;    CCPoint p0, p1, pt0, pt1;    p0 = _hillKeyPoints[_fromKeyPointI];    for (int i = _fromKeyPointI + 1; i < _toKeyPointI + 1; ++i)    {        p1 = _hillKeyPoints[i];        // triangle strip between p0 and p1        int hSegments = floorf((p1.x - p0.x) / kHillSegmentWidth);        float dx = (p1.x - p0.x) / hSegments;        float da = M_PI / hSegments;        float ymid = (p0.y + p1.y) / 2;        float ampl = (p0.y - p1.y) / 2;        pt0 = p0;        _borderVertices[_nBorderVertices++] = pt0;        for (int j = 1; j < hSegments + 1; ++j)        {            pt1.x = p0.x + j * dx;            pt1.y = ymid + ampl * cosf(da * j);            _borderVertices[_nBorderVertices++] = pt1;            _hillVertices[_nHillVertices] = ccp(pt0.x, 0);            _hillTexCoords[_nHillVertices++] = ccp(pt0.x / 5121.0f);            _hillVertices[_nHillVertices] = ccp(pt1.x, 0);            _hillTexCoords[_nHillVertices++] = ccp(pt1.x / 5121.0f);            _hillVertices[_nHillVertices] = ccp(pt0.x, pt0.y);            _hillTexCoords[_nHillVertices++] = ccp(pt0.x / 5120);            _hillVertices[_nHillVertices] = ccp(pt1.x, pt1.y);            _hillTexCoords[_nHillVertices++] = ccp(pt1.x / 5120);            pt0 = pt1;        }        p0 = p1;    }    prevFromKeyPointI = _fromKeyPointI;    prevToKeyPointI = _toKeyPointI;}

這裡的大部分程式碼,跟上面的使用餘弦繪製山丘曲線一樣。新的部分,是將山丘每個區段的頂點用來填充陣列,每個條紋需要4個頂點和4個紋理座標。在draw方法裡面,最上面新增如下程式碼: 

12345678910CC_NODE_DRAW_SETUP();ccGLBindTexture2D(_stripes->getTexture()->getName());ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords);ccDrawColor4F(1.0f, 1.0f, 1.0f, 1.0f);glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, _hillVertices);glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, _hillTexCoords);glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)_nHillVertices);

這裡繫結條紋紋理作為渲染紋理來使用,傳入之前計算好的頂點陣列和紋理座標陣列,然後以GL_TRIANGLE_STRIP來繪製這些陣列。此外,註釋掉繪製山丘直線和曲線的程式碼。在init方法裡面,呼叫generateHills方法之前,新增如下程式碼: 

1this->setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTexture));

開啟HelloWorldScene.cpp檔案,在spriteWithColor1方法裡面,註釋// Layer 4: Noise裡,更改混合方式,程式碼如下: 

1ccBlendFunc blendFunc = {GL_DST_COLOR, CC_BLEND_DST};

編譯執行,可以看到不錯的山丘了,如下圖所示:7.還不完善?仔細看山丘,可能會注意到一些不完善的地方,如下圖所示:增加水平區段數量,可以提高一些質量。開啟Terrain.h檔案,修改kHillSegmentWidth為如下: 

1#define kHillSegmentWidth 5