如何制作一个类似Tiny Wings的游戏 cocos2d-x-3.0alpha0

在第一篇《如何使用CCRenderTexture创建动态纹理》基础上,增加创建动态山丘,原文《How To Create A Game Like Tiny Wings with Cocos2D 2.X Part 1》,在这里继续以Cocos2d-x进行实现。有关源码、资源等在文章下面给出了地址。

步骤如下:
1.使用上一篇的工程;

2.添加地形类Terrain,派生自CCNode类。文件Terrain.h代码如下:

#include "cocos2d.h"

USING_NS_CC;
#define kMaxHillKeyPoints 1000

class Terrain:public Node {
    
public:
    Terrain();
    ~Terrain();
    
    CREATE_FUNC(Terrain);
    void generateHills();
    CC_SYNTHESIZE_RETAIN(Sprite*, _stripes, Stripes);
    
    void draw();
    bool init();
    void setOffsetX(float newOffsetX);
    
private:
    int _offsetX;
    Point _hillKeyPoints[kMaxHillKeyPoints];

};
这里声明了一个 _hillKeyPoints 数组,用来存储每个山丘顶峰的点,同时声明了一个 _offsetX 代表当前地形滚动的偏移量。文件 Terrain.cpp 代码如下: 

Terrain::Terrain()
{
    _stripes = NULL;
    _offsetX = 0;
}

Terrain::~Terrain()
{
    CC_SAFE_RELEASE_NULL(_stripes);
}

void Terrain::generateHills()
{
    Size winSize = Director::getInstance()->getWinSize();
    float x = 0;
    float y = winSize.height / 2;
    for (int i = 0; i < kMaxHillKeyPoints; ++i) {
        _hillKeyPoints[i] = Point(x, y);
        x += winSize.width / 2;
        y = rand() % (int) winSize.height;
    }
}
这个方法用来生成随机的山丘顶峰的点。第一个点在屏幕的左侧中间,之后的每一个点,x轴方向移动半个屏幕宽度,y轴方向设置为0到屏幕高度之间的一个随机值。添加以下方法:

bool Terrain::init()
{
    bool bRet = false;
    do {
        CC_BREAK_IF(!Node::init());
        this->generateHills();
        bRet = true;
    } while (0);
    return bRet;
    
}

void Terrain::draw()
{
    Node::draw();
    for (int i = 1; i < kMaxHillKeyPoints; ++i) {
        DrawPrimitives::drawLine(_hillKeyPoints[i - 1], _hillKeyPoints[i]);
    }
}
init 方法调用 generateHills 方法创建山丘, draw 方法简单地绘制相邻点之间的线段,方便可视化调试。添加以下方法:

void Terrain::setOffsetX(float newOffsetX)
{
    _offsetX = newOffsetX;
    this->setPosition(Point(-_offsetX * this->getScale(), 0));
}
英雄沿着地形的x轴方法前进,地形向左滑动。因此,偏移量需要乘以-1,还有缩放比例。打开 HelloWorldScene.h 文件,添加头文件引用: 

#include "Terrain.h"
添加如下变量: 

 CC_SYNTHESIZE_RETAIN(Terrain*, _terrain, Terrain);
打开 HelloWorldScene.cpp 文件,在 onEnter 方法里,调用 genBackground 方法之前,加入如下代码:

_terrain = Terrain::create();
    this->addChild(_terrain , 1);
update 方法里,最后面添加如下代码: 

_terrain->setOffsetX(offset);
修改 genBackground 方法为如下:

void HelloWorld::genBackground()
{
    if (_background) {
        _background->removeFromParentAndCleanup(true);
    }
    
    Color4F bgColor = this->randomBrightColor();
    _background = this->spriteWithColor(bgColor, 512, 512);
    
    Size winSize = Director::getInstance()->getWinSize();
    _background->setPosition(Point(winSize.width / 2, winSize.height / 2));
    Texture2D::TexParams tp = {GL_LINEAR,GL_LINEAR,GL_REPEAT,GL_REPEAT};
    _background->getTexture()->setTexParameters(tp);
    
    this->addChild(_background);
    
    Color4F color3 = this->randomBrightColor();
    Color4F color4 = this->randomBrightColor();
    
    Sprite* stripes = this->spriteWithColor1(color3, color4, 512, 512, 4);
    Texture2D::TexParams 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 方法为如下: 

void Terrain::generateHills()
{
    Size winSize = Director::getInstance()->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] = Point(x, y);
        if (i == 0) {
            x = 0;
            y = winSize.height / 2;
            
        }
        else
        {
            x += rand() % rangeDX + minDX;
            while (true) {
                dy = rand() % rangeDX + minDX;
                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文件,添加如下变量: 

    int _fromKeyPointI;
    int _toKeyPointI;

打开Terrain.cpp文件,在构造函数里面添加如下代码:

    _fromKeyPointI = 0;
    _toKeyPointI   = 0;
添加如下方法:

void Terrain::resetHillVertices()
{
    Size  winSize = Director::getInstance()->getWinSize();
    
    static int prevFromKeyPointI = -1;
    static int prevToKeyPointI   = -1;
    
    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 方法,代码如下: 

void Terrain::draw()
{
    Node::draw();
    for (int i = MAX(_fromKeyPointI, 1); i < _toKeyPointI; ++i) {
        DrawPrimitives::setDrawColor4F(1.0, 0, 0, 1.0);
        DrawPrimitives::drawLine(_hillKeyPoints[i - 1], _hillKeyPoints[i]);
    }
}
现在,不是绘制所有点,而是只绘制当前可见的点,这些点是前面计算得到的。另外,也把线的颜色改成红色,这样更易于分辨。接着,在 init 方法里面,最后面添加如下代码: 

        this->resetHillVertices();
setOffsetX 方法里面,最后面添加如下代码:

        this->resetHillVertices();

为了更容易看到,打开HelloWorldScene.cpp文件,在onEnter方法,最后面添加如下代码:

this->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 文件,添加区段长度定义,如下代码: 

#define kHillSegmentWidth  10
然后,打开 Terrain.cpp 文件,在 draw 方法里面, ccDrawLine 之后,添加如下代码: 

  DrawPrimitives::setDrawColor4F(1.0f, 1.0f, 1.0f, 1.0f);
        Point p0 = _hillKeyPoints[i - 1];
        Point p1 = _hillKeyPoints[i];
        int hSegment = floorf((p1.x - p0.x) / kHillSegmentWidth);
        float dx = (p1.x - p0.x) / hSegment;
        float da = M_PI / hSegment;
        float ymid = (p0.y + p1.y) / 2;
        float ampl = (p0.y - p1.y) / 2;

        Point pt0,pt1;
        pt0 = p0;

        for (int j = 0; j < hSegment + 1; ++j) {
            pt1.x = p0.x + j * dx;
            pt1.y = ymid + ampl * cosf(da * j);
            DrawPrimitives::drawLine(pt0, pt1);
            pt0 = pt1;
        }
打开 HelloWorldScene.cpp 文件,在 onEnter 方法,设置scale为1.0,如下代码: 
this->setScale(1);
编译运行,现在可以看到一条曲线连接着山丘,如下图所示:

6.绘制山丘。用上一篇文章生成的条纹纹理来绘制山丘。计划是对山丘的每个区段,计算出两个三角形来渲染山丘,如下图所示:

还将设置每个点的纹理坐标。对于x坐标,简单地除以纹理的宽度(因为纹理重复)。对于y坐标,将山丘的底部映射为0,顶部映射为1,沿着条带的方向分发纹理高度。打开 Terrain.h 文件,添加如下代码:

#define kMaxHillVertices 4000
#define kMaxBorderVertices 800
添加类变量,代码如下:

    int _nHillVertices;
    Point _hillVertices[kMaxHillVertices];
    Point _hillTexCoords[kMaxHillVertices];
    
    int _nBorderVertices;
    Point _borderVertices[kMaxBorderVertices];
打开 Terrain.cpp 文件,在 resetHillVertices 方法里面,最后面添加如下代码: 

 if (prevFromKeyPointI != _fromKeyPointI || prevToKeyPointI != _toKeyPointI) {
        
        _nHillVertices = 0;
        _nBorderVertices = 0;
        Point 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] = Point(pt0.x, 0);
                _hillTexCoords[_nHillVertices++] = Point(pt0.x / 512, 1.0f);
                _hillVertices[_nHillVertices] = Point(pt1.x, 0);
                _hillTexCoords[_nHillVertices++] = Point(pt1.x / 512, 1.0f);
                
                
                _hillVertices[_nHillVertices] = Point(pt0.x, pt0.y);
                _hillTexCoords[_nHillVertices++] = Point(pt0.x / 512, 0);
                _hillVertices[_nHillVertices] = Point(pt1.x, pt1.y);
                _hillTexCoords[_nHillVertices++] = Point(pt1.x / 512, 0);

                pt0 = pt1;
            }
           p0 = p1;
        }
        prevFromKeyPointI = _fromKeyPointI;
        prevToKeyPointI = _toKeyPointI;
        
    }
这里的大部分代码,跟上面的使用余弦绘制山丘曲线一样。新的部分,是将山丘每个区段的顶点用来填充数组,每个条纹需要4个顶点和4个纹理坐标。在 draw 方法里面,最上面添加如下代码: 

    CC_NODE_DRAW_SETUP();
    GL::bindTexture2D(_stripes->getTexture()->getName());
    GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POSITION | GL::VERTEX_ATTRIB_FLAG_TEX_COORDS);
  
    DrawPrimitives::setDrawColor4F(1.0f, 1.0f, 1.0f, 1.0f);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, _hillVertices);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORDS, 2, GL_FLOAT, GL_FALSE, 0, _hillTexCoords);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)_nHillVertices);
这里绑定条纹纹理作为渲染纹理来使用,传入之前计算好的顶点数组和纹理坐标数组,然后以 GL_TRIANGLE_STRIP 来绘制这些数组。此外,注释掉绘制山丘直线和曲线的代码。在 init 方法里面,调用 generateHills 方法之前,添加如下代码: 

        this->setShaderProgram(ShaderCache::getInstance()->getProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE));
打开 HelloWorldScene.cpp 文件,在 spriteWithColor1 方法里面,注释 // Layer 4: Noise 里,更改混合方式,代码如下: 

BlendFunc blendFunc = {GL_DST_COLOR,CC_BLEND_DST};
编译运行,可以看到不错的山丘了,如下图所示:

7.还不完善?仔细看山丘,可能会注意到一些不完善的地方,如下图所示:

增加水平区段数量,可以提高一些质量。打开 Terrain.h 文件,修改 kHillSegmentWidth 为如下: 

#define kHillSegmentWidth  5
通过减少每个区段的宽度,强制代码生成更多的区段来填充空间。编译运行,可以看到山丘看起来更好了。当然,代价是处理时间。效果如下图所示:

在第二部分,将会实现海豹飞翔。

参考资料:
1.How To Create A Game Like Tiny Wings with Cocos2D 2.X Part 1 http://www.raywenderlich.com/32954/how-to-create-a-game-like-tiny-wings-with-cocos2d-2-x-part-1

2. 出自 :http://blog.csdn.net/akof1314/article/details/9293797
3.(译)如何制作一个类似tiny wings的游戏:第一部分http://blog.csdn.net/qqmcy/article/details/16846855
非常感谢以上资料,本例子源代码附加资源下载地址http://download.csdn.net/detail/akof1314/5733037

iOS代码例子下载:http://vdisk.weibo.com/s/BDn59yfnBVuM3  
如文章存在错误之处,欢迎指出,以便改正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杜甲同学

感谢打赏,我会继续努力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值