我用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,像下面這樣:

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行):

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

  注釋說,這個成員就是Sprite的四個頂點。V3F_C4B_T2F_Quad又是個啥玩意?看看結構定義(ccTypes.h 291行):

//! 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行):

//! 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,將接口暴露出來(憋了這么久,終於可以敲代碼了)。首先是頭文件:

#ifndef __VERTEX_SPRITE_H__
#define __VERTEX_SPRITE_H__

#include "cocos2d.h"
USING_NS_CC;

class VertexSprite : public Sprite
{
public:
    static VertexSprite* create(const std::string& filename);
    bool initWithFile(const std::string& filename);

    /*
     * 設置四個頂點的坐標
     * @param pTL 左上角頂點坐標
     * @param pBL 左下角頂點坐標
     * @param pTR 右上角頂點坐標
     * @param pBR 右下角頂點坐標
     */
    void SetVertex(const Vec2& pTL, const Vec2& pBL, const Vec2& pTR, const Vec2& pBR);

private:
    VertexSprite(){}
};

#endif // __VERTEX_SPRITE_H__

  然后是實現文件:

#include "VertexSprite.h"

VertexSprite* VertexSprite::create(const std::string& filename)
{
    auto ret = new VertexSprite();

    if (ret->initWithFile(filename))
    {
        ret->autorelease();
    }
    else
    {
        CC_SAFE_DELETE(ret);
    }

    return ret;
}


bool VertexSprite::initWithFile(const std::string& filename)
{
    return Sprite::initWithFile(filename);
}


void VertexSprite::SetVertex(const Vec2& pTL, const Vec2& pBL, const Vec2& pTR, const Vec2& pBR)
{
    // Top Left
    //
    this->_quad.tl.vertices.x = pTL.x;
    this->_quad.tl.vertices.y = pTL.y;
    // Bottom Left
    //
    this->_quad.bl.vertices.x = pBL.x;
    this->_quad.bl.vertices.y = pBL.y;
    // Top Right
    //
    this->_quad.tr.vertices.x = pTR.x;
    this->_quad.tr.vertices.y = pTR.y;
    // Bottom Right
    //
    this->_quad.br.vertices.x = pBR.x;
    this->_quad.br.vertices.y = pBR.y;

    this->setContentSize(Size(0, pTL.y - pBL.y));
}

  可以看到SetVertex方法的最后做了一下setContentSize的操作。為什么呢?因為Sprite繪制的時候,會判斷自己是否在顯示區域內,如果不在,則不繪制(CCSprite.cpp 586行):

// 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的內容如下:

#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類的代碼,首先是頭文件:

#ifndef __BEAT_OBJECT_H__
#define __BEAT_OBJECT_H__

#include "cocos2d.h"
#include "Common.h"
#include "VertexSprite.h"
USING_NS_CC;


class BeatObject : public Node
{
public:
    /*
     * 創建一個BeatObject實例
     * @param pType BeatObject類型,參考BeatObjectType
     * @param pLength BeatObject的長度,僅當該實例為Strip類型時有效
     */
    static BeatObject* create(int pType, float pLength = 0);
    ~BeatObject(){}

public: // Getter
    bool IsBlock();
    bool IsStrip();

public: // Setter
    void setPositionY(float y) override;
    void setRotation(float rotation) override;

private:
    BeatObject();
    bool init(int pType, float pLength = 0);

    // 不允許外部修改BeatObj的坐標
    void setPosition(const Vec2& position){ Node::setPosition(position); }
    void setPositionX(float x){ Node::setPositionX(x); }

private:
    int m_nType;
    Sprite* m_pHead;
    Sprite* m_pTail;
    VertexSprite* m_pBody;
    float m_fLength;
    float m_fCurLength;
};

#endif // __BEAT_OBJECT_H__ 

  實現:

#include "BeatObject.h"

namespace
{
    inline float GetMoveScale(float pY)
    {
        if (pY >= 0)
        {
            return 0;
        }
        return -0.0025f * pY;
    }


    inline bool TypeContains(int pType, const BeatObjectType& pTarType)
    {
        return (pType & pTarType) == pTarType;
    }
}


//////////////////////////////////////////////////////////////////////////
// BeatObject
BeatObject::BeatObject()
    : m_nType(BeatObjectType::Invalid)
    , m_pHead(nullptr)
    , m_pTail(nullptr)
    , m_pBody(nullptr)
    , m_fLength(0)
    , m_fCurLength(0)
{
}


BeatObject* BeatObject::create(int pType, float pLength /* = 0 */)
{
    auto ret = new BeatObject();

    if (ret->init(pType, pLength))
    {
        ret->autorelease();
    }
    else
    {
        CC_SAFE_DELETE(ret);
    }

    return ret;
}


bool BeatObject::init(int pType, float pLength /* = 0 */)
{
    if (!Node::init())
    {
        return false;
    }

    this->m_nType = pType;
    WASSERT(TypeContains(this->m_nType, BeatObjectType::Invalid));

    // 不允許對Block類型設置Length
    // 以及不允許設置Strip類型的Length小於等於0
    //
    if (pLength > 0)
    {
        WASSERT(this->IsStrip());
    }
    else if (pLength < 0)
    {
        WASSERT(false); 
    }
    this->m_fLength = pLength;
 
    if (this->IsStrip())
    {
        this->m_pBody = VertexSprite::create("Strip_Body.png");
        this->m_pTail = Sprite::create("Strip_Tail.png");

        this->m_pBody->setAnchorPoint(Vec2(0.5f, 0));

        this->addChild(this->m_pBody);
        this->addChild(this->m_pTail);
        this->m_pTail->setVisible(false);
    }  
    
    this->m_pHead = Sprite::create("Block.png");
    this->addChild(this->m_pHead);

    if (TypeContains(this->m_nType, BeatObjectType::Star))
    {
        auto s = Sprite::create("Star.png");
        s->setPosition(this->m_pHead->getContentSize() / 2);
        this->m_pHead->addChild(s);
    }

    if (TypeContains(this->m_nType, BeatObjectType::SameTime))
    {
        auto st = Sprite::create("SameTime.png");
        st->setPosition(this->m_pHead->getContentSize() / 2);
        this->m_pHead->addChild(st);
    }

    return true;
}


bool BeatObject::IsBlock()
{
    return TypeContains(this->m_nType, BeatObjectType::Block);
}


bool BeatObject::IsStrip()
{
    return TypeContains(this->m_nType, BeatObjectType::Strip);
}


void BeatObject::setPositionY(float y)
{
    Node::setPositionY(y);
    // 設置圓圈的縮放。若縮放太小直接不顯示
    //
    auto headScale = GetMoveScale(y);
    this->m_pHead->setScale(headScale);
    this->m_pHead->setVisible(headScale > 0.05f);
    // 如果該物件是一個Strip,則需要處理其身體和尾部
    //
    if (this->IsStrip())
    {
        // 模擬無限遠處飛來的效果,保證尾部的y坐標小於0
        //
        if (y + this->m_fLength > 0)
        {
            this->m_fCurLength = -y;
        }
        else if (this->m_fCurLength != this->m_fLength)
        {
            this->m_fCurLength = this->m_fLength;
            this->m_pTail->setPositionY(this->m_fLength);
        }

        auto tailScale = GetMoveScale(this->getPositionY() + this->m_fCurLength);
        this->m_pTail->setScale(tailScale);       
        this->m_pTail->setVisible(tailScale > 0.05f);   
        
        auto harfHeadWidth = headScale * 124 / 2.0f;
        auto harfTailWidth = tailScale * 124 / 2.0f;

        this->m_pBody->SetVertex(
            Vec2(-harfTailWidth, this->m_fCurLength),
            Vec2(-harfHeadWidth, 0),
            Vec2(harfTailWidth, this->m_fCurLength),
            Vec2(harfHeadWidth, 0));
    }
}


void BeatObject::setRotation(float rotation)
{
    this->m_pHead->setRotation(rotation);
}

  

  BeatObject類開放並重寫setPositionY方法,屏蔽setPosition和setPositionX,不允許外部直接修改BeatObject的坐標。這里還重寫了setRotation方法,但是並沒有調用基類的setRotation,原因后面來講。

  

  BeatObject類目前就是這樣了,今后需要其他功能再逐漸添加。接下來我們創建BeatObject的父節點類:BeatObjectColume。

 

  每個物件在運動的時候,都不會離開它們所在的列。換句話說,就是一個在第一列的物件,在任何時候都不會跑到第二列去。BeatObjectColume類就表示一個列。這個類目前比較簡單,我就直接放代碼了,頭文件:

#ifndef __BEAT_OBJECT_COLUME_H__
#define __BEAT_OBJECT_COLUME_H__

#include "cocos2d.h"
#include "BeatObject.h"

USING_NS_CC;

class BeatObjectColume : public Node
{
public:
    CREATE_FUNC(BeatObjectColume);
    ~BeatObjectColume();

public:
    void AddBeatObject(BeatObject* pObj);
    void ClearObjects();
    void SetObjectPositionY(int pIndex, float pY);

private:
    void addChild(Node *child){ Node::addChild(child); }

private:
    BeatObjectColume();
    bool init();

private:
    std::vector<BeatObject*> m_BeatObjList;
};


#endif // __BEAT_OBJECT_COLUME_H__ 

  實現:

#include "BeatObjectColume.h"

BeatObjectColume::BeatObjectColume()
{
}


bool BeatObjectColume::init()
{
    if (!Node::init())
    {
        return false;
    }

    return true;
}


void BeatObjectColume::AddBeatObject(BeatObject* pObj)
{
    pObj->setRotation(-this->getRotation());
    this->addChild(pObj);
    this->m_BeatObjList.push_back(pObj);
}


void BeatObjectColume::ClearObjects()
{
    for (auto it : this->m_BeatObjList)
    {
        it->removeFromParent();
    }

    this->m_BeatObjList.clear();
}


void BeatObjectColume::SetObjectPositionY(int pIndex, float pY)
{
    WASSERT(pIndex >= 0 && pIndex < this->m_BeatObjList.size());

    this->m_BeatObjList.at(pIndex)->setPositionY(pY);
}


BeatObjectColume::~BeatObjectColume()
{
    this->m_BeatObjList.clear();
} 

  注意在AddBeatObject這個方法中,對添加進去的BeatObject進行了一個旋轉處理。為什么呢?因為列是呈扇形分開的,除開最中間的列,其他列都進行過旋轉處理。而我們看視頻截圖:

 

  可以看出在列旋轉后,對於Block物件,它相對於屏幕其實是沒有旋轉的,對於Strip物件(圖中沒有),它的頭對於屏幕也是沒有旋轉的。所以在添加BeatObject的時候,要對它進行一個和Colume方向相反,大小相同的旋轉。而因為Strip物件只有頭部進行了旋轉,所以上文說的不必調用基類的setRotation。

 

  然后我們在LiveScene類中加入一些東西,看看我們實現的成果(因為是臨時使用的代碼,變量使用不太規范)。頭文件:

#ifndef __LIVE_SCENE_H__
#define __LIVE_SCENE_H__

#include "cocos2d.h"
#include "BeatObject.h"
#include "BeatObjectColume.h"

USING_NS_CC;

class LiveScene : public cocos2d::Layer
{
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();

    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();  
    
    // implement the "static create()" method manually
    CREATE_FUNC(LiveScene);

private:
    void update(float dt);

private:
    BeatObjectColume* m_pColume;
};

#endif // __LIVE_SCENE_H__

  實現:

#include "LiveScene.h"


Scene* LiveScene::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();
    
    // 'layer' is an autorelease object
    auto layer = MainScene::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

// on "init" you need to initialize your instance
bool LiveScene::init()
{ 
    if (!Layer::init())
    {
        return false;
    }
    // 加入背景圖
    //
    auto bg = Sprite::create("bg.jpg");
    bg->setPosition(480, 320);
    this->addChild(bg);
    // 加上黑色半透明蒙層
    //
    auto colorLayer = LayerColor::create(Color4B(0, 0, 0, 192));
    this->addChild(colorLayer);
    // 加上一個列
    // 
    this->m_pColume = BeatObjectColume::create();
    this->m_pColume->setPosition(480, 480);
    this->addChild(this->m_pColume);
    // 添加一個BeatObject
    // 如果要添加Block類的Object,則
    // auto obj = BeatObject::create(BeatObjectType::Block);
    auto obj = BeatObject::create(BeatObjectType::Strip | BeatObjectType::SameTime, 256);
    this->m_pColume->AddBeatObject(obj);

    this->scheduleUpdate();

    return true;
}


float moveY = 0;
void LiveScene::update(float dt)
{
    this->m_pColume->SetObjectPositionY(0, moveY);

    moveY -= 4;
    if (moveY < -960)
    {
        moveY = 0;
    }
}

  編譯運行,如果沒有問題的話,看到這樣的效果:

 

 

  這一章就結束了。下一章我們來編寫一個BeatObjectManager,將所有的Object管理起來,並通過數據顯示我們想要的譜面。

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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM