我用Cocos2d-x模拟《Love Live!学院偶像祭》的Live场景(二)

  上一章分析了Live场景中各个元素的作用,这一章开始来分析最关键的部分——打击物件的实现。

 

  上一章放出的视频很模糊,先来看一个清晰版的复习一下:http://www.bilibili.com/video/av1087466/

  

  然后说一下我使用的环境:Win8.1 + VS2013 + Cocos2d-x3.2

  

  接下来为后文作点准备工作:

  1、  创建一个空的cocos2d-x项目;

  2、  把HelloWorldScene类和它的两个源码文件改名。我使用的名称是LiveScene;

  3、  删掉LiveScene类中多余的代码,比如添加一个“Hello World”的Label这种的(说实话cocos2d-x创建空项目每次都要带个这个类挺蛋疼,因为99.99999%的情况我们的项目中根本不需要它,直接创建一个干净的空项目多好);

  4、  删掉Resource文件夹中的所有文件;

  5、  在AppDelegate类中修改设计分辨率为960×640,像下面这样:

1
2
3
4
5
6
7
8
9
10
bool AppDelegate::applicationDidFinishLaunching() {
    // ...
    if(!glview) {
        glview = GLView::create("My Game");
        glview->setFrameSize(960, 640);
        director->setOpenGLView(glview);
        glview->setDesignResolutionSize(960, 640, ResolutionPolicy::SHOW_ALL);
    }
    // ...
}

  准备工作做完后,可以先编译运行一下。如果跑起来一片黑,那么就OK可以继续了。  

 

  准备工作完成,开始分析打击物件。从视频看,打击物件有个3D透视效果:近大远小。Cocos2d-x 3.x的版本已经支持3D模型,可以制作3D游戏了。但是对于这个比较简单的效果,直接上3D有点大炮打蚊子的感觉。可以运用3D透视公式,根据物件的Z轴距离计算出在屏幕上的X和Y坐标以及缩放。

 

  等等,仔细看看那个视频,感觉物件飞过来的过程,和真正的3D比还是有点违和啊……于是,LL(《Love Live!学院偶像祭》,以后都简称LL)中真的是用的3D吗?

 

  此时需要检测一下了。先说一下我的思路:对于下图所示的三个三连打物件,他们的时间间隔是一样的。如果使用了3D透视,那么后两个的屏幕坐标间距一定小于前两个,近大远小嘛。

  还是不大能理解?来看看这么一张图(图片源自网络):

 

  不要吐槽远景那些莫名其妙的东西……我们来看铁轨,是不是从画面上看,越远处的枕木越密集?但是实际上枕木和枕木之间的间距是一样的。这个现象用计算机图形学的术语讲,其实就是世界坐标系到摄像机坐标系的变换形成的。因为屏幕上没有Z轴,只有X和Y,物体的Z轴变化只能通过移动位置和缩放来表现。

 

  扯远了,用这一点来验证LL是否使用了3D透视,就是因为物件飞过来的时候,它们的Z轴速度肯定是一定的(从一个音乐游戏来讲,如果是变速运动这游戏基本上没法玩了),而三个物件的时间间隔相同,那么经过坐标变换必然形成后两个的屏幕坐标间距一定小于前两个。

 

  那么打开神奇PS,将三个圆的中心标出来,连上线。不会PS没关系,我把图做好了。背景有点花?盖个黑色图层:

 

  能明显看出,三个圆之间的屏幕距离是相等的。于是乎,这个效果根本没有使用3D透视变换。也就是说,物件在飞向屏幕的过程中,在屏幕上移动的速度也是不变的。

     

  于是事情变得简单了,不需要去计算透视变换了。对于单个的一列,物件飞过来就是X轴不变,Y轴匀速运动。再从飞出来的点拉两条射线出来:

 

  艾玛,缩放也是匀变化的,取值就是一个f(x) = kx + b的一元线性方程嘛。接下来我们来求这个方程的参数k和b。

 

   为了方便测量,以最中间的按钮为准。从视频看这个游戏的长宽比是3:2的,所以文章开始说的的设计分辨率要取960×640。测量值写在图片上了(怎么测量属于PS的应用,与这一系列文章无关,这里就不做介绍了):

 

 

  所以就可以知道:物件在0px高度的时候,缩放是0,在-400px高度的时候,缩放是1(这里定义列的锚点是物件出现的点,以方便后续做旋转操作,所以物件的y是从0开始逐渐变小的)。所以可以求得缩放方程式是

    scale = -0.0025 * y

  可以看出对于圆圈物件,就是一个Sprite对象。对于长条物件,是一个Sprite做头,一个Sprite做中间,一个Sprite做尾。

        

  对于圆圈物件来说,我只要处理缩放和坐标就行。而对于Sprite的中间,则是一个随时在变化的梯形。

 

  Cocos2d-x提供了setSkew方法对Sprite进行扭曲,但是这个扭曲只是一个平行四边形变换,并不是梯形。我们知道OpenGL渲染图形是先渲染顶点,再渲染像素的。所以修改Sprite的四个顶点可以达到想要的效果。说到顶点,自然就想到了顶点着色器,想到了GLSL。不过,这个效果怎么说也不复杂,杀鸡焉用牛刀呢。其实,在Sprite类中有一个成员(CCSprite.h 563行):

1
2
// vertex coords, texture coords and color info
V3F_C4B_T2F_Quad _quad;

  注释说,这个成员就是Sprite的四个顶点。V3F_C4B_T2F_Quad又是个啥玩意?看看结构定义(ccTypes.h 291行):

1
2
3
4
5
6
7
8
9
10
11
12
//! 4 Vertex3FTex2FColor4B
struct V3F_C4B_T2F_Quad
{
    //! top left
    V3F_C4B_T2F    tl;
    //! bottom left
    V3F_C4B_T2F    bl;
    //! top right
    V3F_C4B_T2F    tr;
    //! bottom right
    V3F_C4B_T2F    br;
};

  里面果然是四个成员,分别表示左上,左下,右上,右下四个顶点。而顶点的结构V3F_C4B_T2F是这样的(ccTypes.h 245行):

1
2
3
4
5
6
7
8
9
10
11
12
//! a Vec2 with a vertex point, a tex coord point and a color 4B
struct V3F_C4B_T2F
{
    //! vertices (3F)
    Vec3     vertices;            // 12 bytes
 
    //! colors (4B)
    Color4B      colors;              // 4 bytes
 
    // tex coords (2F)
    Tex2F        texCoords;           // 8 bytes
};

  这三个成员分别表示:顶点坐标,顶点颜色和UV纹理贴图坐标。我们只需要处理位置即可。然而,_quad成员是一个protected对象,Sprite类也没有对外提供访问接口。因为对象时protected的,我们可以派生一个Sprite类的子类,提供访问_quad的接口。

 

  在项目中添加VertexSprite.h和VertexSprite.cpp,将接口暴露出来(憋了这么久,终于可以敲代码了)。首先是头文件:

  然后是实现文件:

  可以看到SetVertex方法的最后做了一下setContentSize的操作。为什么呢?因为Sprite绘制的时候,会判断自己是否在显示区域内,如果不在,则不绘制(CCSprite.cpp 586行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// draw
 
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    // Don't do calculate the culling if the transform was not updated
    _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
 
    if(_insideBounds)
    {
        _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform);
        renderer->addCommand(&_quadCommand);
#if CC_SPRITE_DEBUG_DRAW
        _customDebugDrawCommand.init(_globalZOrder);
        _customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);
        renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
    }
}

  所以设置顶点坐标后,还需要手动设置它的_contentSize。如果不设置,这个值默认就是我们使用的贴图的大小,即1px × 1px。所以在没设置的情况下,这个Sprite稍微移出显示区域一点,整个Sprite就不会显示了。所以我们需要在设定顶点后,手动去修改它的_contentSize。为了节约运算资源,以及考虑到可能出现的情况(只会是梯形,不会出现凹四边形等情况),这里直接设置_contentSize的高度就行了,可以减少一定的运算量。

 

  目前只写了一个从文件创建对象的create方法。当然,今后为了各种需求可以也可以加入其他的比如createWithSpriteFrame等等。

 

  有了一个可设定顶点坐标的类,接下来就可以编写物件类:BeatObject类了。根据框架设计,画面表现部分和数据部分是分开的,所以BeatObject类不能存放这个BeatObject出现的时间等数据,外部仅能改变BeatObject的位置。

  

  当外部调用BeatObject的setPositionY方法(物件只会纵向移动,不应当修改x坐标)时,应当会做如下操作:

    1、  计算出头的缩放值;

    2、  如果这个BeatObject的类型是Strip,则根据BeatObject的长度计算出尾部的坐标和缩放值;

    3、  如果这个BeatObject的类型是Strip,再计算出中间部分的四个顶点坐标。

 

   四个顶点的坐标和尾部的缩放怎么计算呢?请看图:

  

  如图所示是一个Strip物件的示意图。下面的圆是头部,上面的圆是尾部,中间红色的梯形就是我们要进设置顶点的中间部。TL, BL, TR, BR则是四个顶点,直接对应_quad成员中的四个成员。

 

  Length值是由外部设置进去的,表示头部圆心到尾部圆心的长度,也就是梯形的高。头部圆的缩放的公式上文推导出了,尾部圆的缩放公式则是

    -0.0025 * (y + length)

  y值就是这个坐标系中的原点相对于Colume节点的高度。

 

  我使用的图中,头部尾部的图像虽然是128px×128px,但是圆圈本身有个外发光,导致圆圈实际没有128px那么大。这里我取124px作为中间部一倍缩放宽度。实际制作的时候,这个宽度值应当根据使用的图像作出适当调整。于是,四个顶点的坐标取值如下:

    TL:  x = -尾部缩放 × 124 / 2, y = length

    BL:  x = -头部缩放 × 124 / 2, y = 0

    TR:  x = 尾部缩放 × 124 / 2, y = length

    BR:  x = 头部缩放 × 124 / 2, y = 0

  

  我使用一个enum来区分BeatObject的类型。这个enum存放在Common.h中,因为在使用物件数据的时候还会用上。圈叫Block因为在传统下落式音乐游戏中那玩意叫“块”。Common.h的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef __COMMON_H__
#define __COMMON_H__
 
 
enum BeatObjectType : int
{
    Invalid   = 0x0000,
    Block     = 0x0001,
    Strip     = 0x0002,
    SameTime  = 0x0004,
    Star      = 0x0008
};
 
#define WASSERT(__COND__) if(!(__COND__)){ DebugBreak(); }
 
#endif // __COMMON_H__

  

  WASSERT宏用于在断言符合时产生一个断点,而不是生成报错对话框,这样可以方便调试。

 

  如下是BeatObject类的代码,首先是头文件:

  实现:

  

  BeatObject类开放并重写setPositionY方法,屏蔽setPosition和setPositionX,不允许外部直接修改BeatObject的坐标。这里还重写了setRotation方法,但是并没有调用基类的setRotation,原因后面来讲。

  

  BeatObject类目前就是这样了,今后需要其他功能再逐渐添加。接下来我们创建BeatObject的父节点类:BeatObjectColume。

 

  每个物件在运动的时候,都不会离开它们所在的列。换句话说,就是一个在第一列的物件,在任何时候都不会跑到第二列去。BeatObjectColume类就表示一个列。这个类目前比较简单,我就直接放代码了,头文件:

  实现:

  注意在AddBeatObject这个方法中,对添加进去的BeatObject进行了一个旋转处理。为什么呢?因为列是呈扇形分开的,除开最中间的列,其他列都进行过旋转处理。而我们看视频截图:

 

  可以看出在列旋转后,对于Block物件,它相对于屏幕其实是没有旋转的,对于Strip物件(图中没有),它的头对于屏幕也是没有旋转的。所以在添加BeatObject的时候,要对它进行一个和Colume方向相反,大小相同的旋转。而因为Strip物件只有头部进行了旋转,所以上文说的不必调用基类的setRotation。

 

  然后我们在LiveScene类中加入一些东西,看看我们实现的成果(因为是临时使用的代码,变量使用不太规范)。头文件:

  实现:

  编译运行,如果没有问题的话,看到这样的效果:

 

 

  这一章就结束了。下一章我们来编写一个BeatObjectManager,将所有的Object管理起来,并通过数据显示我们想要的谱面。

  本章所用到的资源:http://pan.baidu.com/s/1dDk0TXZ

 

 

posted @   GuyaWeiren  阅读(1636)  评论(15编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
点击右上角即可分享
微信分享提示