Tuesday, June 3, 2014

Bài 21: Học làm game thứ 3: Sushi Crush - Like Candy Crush or Bejewer ( Part 2 )

Hi mọi người!

Chúng ta cùng tiếp tục bài học về game Sushi Crush nhé. Tổng kết lại bài trước 1 chút:
+ Tạo Class mới ( Sushi )
+ Tạo ma trận Sushi
+ Tạo Action rơi các sushi xuống ở màn chơi

Trong bài này chúng ta sẽ tập trung nghiên cứu 1 số vấn đề sau đây:

+ Kiểm tra và "ăn" 1 dãy ( 3-4-5 ) Sushi cùng loại
+ Tạo hiệu ứng "nổ" khi ăn Sushi
+ Lấp đầy khoảng trống do các Sushi đã bị ăn để lại trên ma trận

Tất cả các vấn đề trên đây đều xảy ra trong lớp PlayLayer nhé các bạn, lớp SushiSprite tạm thời sẽ không phải động tới Code.

(Bài này khá dài do code cũng dài, nên chỗ nào sai chính tả thì bỏ qua vậy)

Bắt đầu thôi!

Mở file PlayLayer.h. thêm code như sau

Phần Public:

    virtual void update(float dt) override; // Hàm này update game Scene theo thời gian dt ( 1/60 ở file AppDelegate.cpp đó)

Phần Private:

    bool m_isAnimationing; // biến kiểm tra việc đang ăn, rơi, hay hành động khác của Sushi hay không
    void checkAndRemoveChain(); // Kiểm tra và ăn dãy Sushi
    void getColChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList); // Kiểm tra tồn tại dãy Sushi theo cột hay không? - Lấy ra 1 List Sushi giống nhau ( kiểu &chainList là kiểu tham chiếu trong C+, dùng để thay đổi tham số truyền vào hàm thông qua việc lấy địa chỉ. Tuy giống con trỏ, nhưng nó có điểm khác con trỏ là ko phải dùng dấu *tên biến  để thao tác mà dùng trực tiếp tên biến )
    void getRowChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList); // Kiểm tra tồn tại dãy Sushi theo hàng hay không, Lấy ra bởi List
    void removeSushi(std::list<SushiSprite *> &sushiList);  // Xóa bỏ List Sushi, Ăn chuỗi Sushi
    void explodeSushi(SushiSprite *sushi); // Hiệu ứng nổ khi ăn Sushi
    void fillVacancies(); // Điền đầy khoảng trống do dãy Sushi đã bị ăn mất

B1 - Kiểm tra và ăn dãy Sushi cùng loại theo hàng và cột

Trước tiên, Mở file PlayLayer.cpp, thay đổi một số đoạn Code sau

// Kích thước ma trận 7x9
#define MATRIX_WIDTH (7)
#define MATRIX_HEIGHT (9)

PlayLayer::PlayLayer()
: spriteSheet(NULL)
, m_matrix(NULL)
, m_width(0)
, m_height(0)
, m_matrixLeftBottomX(0)
, m_matrixLeftBottomY(0)
, m_isAnimationing(true)  // Thêm vào hàm tạo
{
}

Trong hàm init(), thêm lệnh 

    scheduleUpdate(); // Update Scene theo thời gian

ở cuối trước return true;

Có lệnh scheduleUpdate(); thì phải có Hàm này để update theo dt


void PlayLayer::update(float dt)
    // Kiểm tra giá trị lần đầu của m_isAnimationing, mỗi bước thời gian dt, sẽ lại kiểm tra m_isAnimationing là true hay flase
    if (m_isAnimationing) { // nếu True
        // Gán = false
        m_isAnimationing = false;
        
        // Duyệt trong toàn ma trận 
        for (int i = 0; i < m_height * m_width; i++) {
            SushiSprite *sushi = m_matrix[i];

            // Nếu tồn tại 1 Sushi mà đang có "Action" thì  m_isAnimationing = true, và thoát vòng lặp
            if (sushi && sushi->getNumberOfRunningActions() > 0) {
                m_isAnimationing = true;
                break;
            }
        }
    }

    // Đến khi không có Action nào của Sushi tại bước thời gian dt nào đó, thì kiểm tra việc "Ăn" dãy Sushi nếu tồn tại

    if (!m_isAnimationing) { 
        checkAndRemoveChain();
    }
}

Kiểm tra việc ăn dãy Sushi giống nhau

void PlayLayer::checkAndRemoveChain()
{

    // Duyệt ma trận
    for (int i = 0; i < m_height * m_width; i++) {
        SushiSprite *sushi = m_matrix[i];

        if (!sushi) { // Rỗng thì bỏ qua
            continue;
        }
        
        // Đếm số lượng Sushi tạo thành chuỗi
        
        // Tạo 1 List để chứa các Sushi giống nhau
        std::list<SushiSprite *> colChainList; // Chuỗi Sushi theo cột
        getColChain(sushi, colChainList); // Lấy ra Chuỗi Sushi giống nhau theo cột, chú ý chỗ này ko còn dấu & giống như ở phần khai báo trong file .h, đây là cách dùng biến tham chiếu trong C++
        
        std::list<SushiSprite *> rowChainList; // Chuỗi Sushi theo hàng
        getRowChain(sushi, rowChainList); // Lấy ra Chuỗi Sushi giống nhau theo hàng

        // &longerList = biến tham chiếu
        // So sánh chuỗi dọc và chuỗi ngang, gán chuỗi lớn hơn cho longerList, tại sao lại có chỗ này, vì chỗ này vẫn trong vòng lặp với thứ tự Sushi thứ i có thể sẽ tồn tại 1 dấu CỘNG tạo bởi 2 chuỗi dọc và ngang cùng chứa Sushi i . Chơi candy, bejewer là biết
        std::list<SushiSprite *> &longerList = colChainList.size() > rowChainList.size() ? colChainList : rowChainList;

        // Chuỗi longer có 3 Sushi thì xóa bỏ chuỗi đó
        if (longerList.size() == 3) {
            removeSushi(longerList); // Ăn thôi
            return;
        }
        if (longerList.size() > 3) {
            // Tạo 1 Sushi Đặc biệt ở đây
            removeSushi(longerList);
            return;
        }
    }
}


Ta cùng tìm và kiểm tra sự tồn tại của Chuỗi các Sushi giống nhau theo hàng và cột trong ma trận qua 2 hàm sau đây

void PlayLayer::getColChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList)
{
    chainList.push_back(sushi); // Thêm vào dãy Sushi đầu tiên, tại vị trí thứ i đang xét trong vòng lặp FOR của hàm checkAndRemoveChain
 
    int neighborCol = sushi->getCol() - 1; // Xét cột bên trái
    while (neighborCol >= 0) { // Tồn tại cột bên trái

        // Tạo 1 pointer Sushi "bên trái" trỏ vào Sushi tại vị trí  (Hàng * width + neighborCol ), đây là cách quy ma trận cấp 2  về mảng 1 chiều nhé
        SushiSprite *neighborSushi = m_matrix[sushi->getRow() * m_width + neighborCol];

        // Nếu tồn tại sushi bên trái và cùng imgIndex (cùng loại Sushi) với sushi đang xét thì..
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            // Thêm sushi trái này vào list
            chainList.push_back(neighborSushi);
            neighborCol--; // Xét tiếp Sushi bên trái đến khi ko còn Sushi nào, cột 0
        } else {
            break;  // Ko thỏa mãn đk if ở trên, Phá vòng while
        }
    }
 
    neighborCol = sushi->getCol() + 1; // Xét Sushi bên phải
    while (neighborCol < m_width) { // Xét đến cột cuối cùng, cột cuối = m_width - nhé
        // Tương tự trên tìm ông sushi cùng loại bên trái
        SushiSprite *neighborSushi = m_matrix[sushi->getRow() * m_width + neighborCol];
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            chainList.push_back(neighborSushi); // Nhét vào List
            neighborCol++;
        } else {
            break; // Phá vòng while
        }
    }
}


// Giải thích Tương tự getColChain nhỉ
void PlayLayer::getRowChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList)
{
    chainList.push_back(sushi);
 
    int neighborRow = sushi->getRow() - 1; // Xét sushi bên dưới
    while (neighborRow >= 0) {
        SushiSprite *neighborSushi = m_matrix[neighborRow * m_width + sushi->getCol()];
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            chainList.push_back(neighborSushi);
            neighborRow--;
        } else {
            break;
        }
    }
 
    neighborRow = sushi->getRow() + 1; // Xét sushi bên trên
    while (neighborRow < m_height) {
        SushiSprite *neighborSushi = m_matrix[neighborRow * m_width + sushi->getCol()];
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            chainList.push_back(neighborSushi);
            neighborRow++;
        } else {
            break;
        }
    }
}


Và đây là hàm ĂN Sushi ( đọc lại hàm checkAndRemoveChain() để hiểu rõ cơ chế)

void PlayLayer::removeSushi(std::list<SushiSprite *> &sushiList)
{

    m_isAnimationing = true;
 
    std::list<SushiSprite *>::iterator itList; // Con trỏ duyệt trong List

    // Cú pháp vòng For duyệt 1 List trong C++ nâng cao
    for (itList = sushiList.begin(); itList != sushiList.end(); itList++) {

        // Chỗ này có vẻ hơi khó hiểu do nhiều dấu * nhỉ, Thế này nhé SushiSprite *sushi là tạo ra 1 con trỏ kiểu SushiSprite, (SushiSprite *) là ép kiểu con trỏ, *itList là giá trị chứa trong con trỏ, vì giá trị này lại là 1 con trỏ nên mới có việc ép kiểu con trỏ (SushiSprite *). iList là 1 con trỏ lại duyệt 1 mảng con trỏ nên có vẻ phức tạp thế này. Bạn hãy đọc lại về mảng con trỏ trong C++ là có thể hiểu

        SushiSprite *sushi = (SushiSprite *)*itList;
        // Loại bỏ sushi i ra khỏi ma trận
        m_matrix[sushi->getRow() * m_width + sushi->getCol()] = NULL;
        explodeSushi(sushi); // Tạo hiệu ứng nổ
    }
 
    // Rơi xuống để lấp đầy chỗ trống tạo bởi Sushi đã bị ăn
    fillVacancies();
}

Các hạm này sử dụng để kiểm tra và ăn các chuỗi Sushi giống nhau ( >3 ). Ta hãy cùng nghiên cứu 2 phần nhỏ sau

B2 - Tạo hiệu ứng nổ khi ăn Sushi

Vẫn trong PlayLayer.cpp, Bạn thêm 1 hàm này

void PlayLayer::explodeSushi(SushiSprite *sushi)
{

    // Thời gian hiệu ứng 0,3 giây
    float time = 0.3;

    // Thực hiện 2 hành động tuần tự, Co Sushi về kích thước, 0, 0, sau đó tự remove khỏi Contener cha
    sushi->runAction(Sequence::create(
                                      ScaleTo::create(time, 0.0), // Co kích thước về 0 trong thời gian 0.3
                                      CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, sushi)),
                                      NULL));
 
     // Action của Sprite tròn, mô phỏng vụ nổ

     auto circleSprite = Sprite::create("circle.png"); // Tạo mới sprite tròn
     addChild(circleSprite, 10);
     circleSprite->setPosition(sushi->getPosition()); // Vị trí = vị trí Sushi
     circleSprite->setScale(0); // Kích thước đầu =0
     // Thực hiện hành động tuần tự sau, Tăng kích thước lên tỷ lệ 1.0 trong thời gian 0,3 giây, sau đó xóa khỏi Layer
     circleSprite->runAction(Sequence::create(ScaleTo::create(time, 1.0),
                                             CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, circleSprite)),
                                             NULL));

     // 3. Tạo hiệu ứng particleStars, CHÚ Ý

     auto particleStars = ParticleSystemQuad::create("stars.plist"); // Tạo mới
     particleStars->setAutoRemoveOnFinish(true); // Tự động remove khi xong việc
     particleStars->setBlendAdditive(false); // Thiết lập sự pha trộn thêm vào = false

     particleStars->setPosition(sushi->getPosition()); // Đặt vị trí tại Sushi nổ
     particleStars->setScale(0.3);  //  Thiết lập tỉ lệ 0.3
     addChild(particleStars, 20); // Thêm vào Layer Play 
}

Vâng hiệu ứng nổ và 1 chút màu mè đẹp mắt chỉ có vậy thôi. Tiếp theo là phần cho các Sushi rơi xuống điền đầy vào chỗ trống của các Sushi đã bị ăn

B3 - Lấp đầy khoảng trống do Sushi bị ăn để lại trên Ma trận

void PlayLayer::fillVacancies()
{
    Size size = CCDirector::getInstance()->getWinSize();
    // Chỗ này nhìn có vẻ phức tạp nhưng chẳng có gì đâu, chỉ là khai báo con trỏ, cấp phát bộ nhớ cho nó thôi, dùng như mảng 1 chiều
    int *colEmptyInfo = (int *)malloc(sizeof(int) * m_width);
    memset((void *)colEmptyInfo, 0, sizeof(int) * m_width); // set giá trị là 0 hết
 
    // Rơi Sushi đang có xuống khoảng trống
    SushiSprite *sushi = NULL; // Tạo 1 con trỏ Sushi = Null, 

    // Duyệt ma trận. Lưu ý ở đây 1 chút, chúng ta thường duyệt mảng 2 chiều theo thứ tự hàng, rồi đến cột, nhưng ở đây, hơi ngược 1 tý là cột rồi đến hàng. Và lưu ý rằng Cột 0, và Hàng 0 nằm ở vị trí bên Dưới phía Trái nhé. khi tạo ma trận ta cho viên Sushi 0,0 rơi xuống trước tiên mà

    for (int col = 0; col < m_width; col++) { // Duyệt theo cột, từ trái sang phải
        int removedSushiOfCol = 0;

        // Duyệt theo hàng, từ dưới lên trên
        for (int row = 0; row < m_height; row++) {
            sushi = m_matrix[row * m_width + col]; // Sushi tại vị trí hàng, cột
            if (NULL == sushi) { // Nếu rỗng
                removedSushiOfCol++; // Đếm số Sushi đã bị "ăn"
            } else { // Nếu ko rỗng
                if (removedSushiOfCol > 0) { // Nếu bên dưới nó có ô trống = số Sushi bị ăn
                    // Làm rơi xuống
                    int newRow = row - removedSushiOfCol; //Vị trí hàng mới ( giảm xuống )
                    // Trong ma trận ta bỏ sushi ở hàng row, và chuyển nó xuống dưới qua removedSushiOfCol ô rỗng
                    m_matrix[newRow * m_width + col] = sushi;
                    m_matrix[row * m_width + col] = NULL;
                    //Di chuyển
                    Point startPosition = sushi->getPosition();
                    Point endPosition = positionOfItem(newRow, col);
                    float speed = (startPosition.y - endPosition.y) / size.height; // Tốc độ
                    sushi->stopAllActions(); // Dừng mọi chuyển động trước đó của Sushi
                    sushi->runAction(MoveTo::create(speed, endPosition)); // Di chuyển = rơi xuống
                    // set hàng mới cho Sushi tại vị trí mới này
                    sushi->setRow(newRow);
                }
            }
        }
     
        // Mảng lưu trữ số lượng Sushi bị ăn tại vị trí Cột xác định
        colEmptyInfo[col] = removedSushiOfCol;
    }
 
    // 2. Tạo mới và làm rơi các Sushi xuống khoảng trống , lấp đầy ma trận
    for (int col = 0; col < m_width; col++) { // Duyệt cột từ trái sang phải

        // Duyệt hàng, chỉ xét từ vị trí rỗng trở lên
        for (int row = m_height - colEmptyInfo[col]; row < m_height; row++) {
            createAndDropSushi(row, col); // Tạo Sushi và rơi xuống vị trí Row, Col
        }
    }
 
    free(colEmptyInfo); // Giải phóng con trỏ 
}

Để dễ hình dung về việc rơi và điền đầy khoảng trống, hãy xem hình ảnh sau đây


Xong rồi, giờ bạn có thể Build và run code được rồi đó.

Tuy nhiên, không có điều gì xảy ra cả, kết quả ra vẫn gần giống bài trước thôi ( trừ trường hợp nào vào màn chơi mà đã ăn được Sushi tự nhiên )

Tổng kết lại trong bài này chúng ta học được 1 số điều thú vị sau đây:

+ Kiểm tra dãy Sushi cùng loại
+ Ăn khi thỏa mãn điều kiện dãy đó >=3 Sushi
+ Tạo hiệu ứng nổ khi ăn
+ Rơi các Sushi lấp đầy khoảng trống
+ Làm việc với List
+ Ma trận 2 chiều, 1 chiều, cách quy đổi 2 chiều sang 1 chiều
+ Làm quen với hệ thống trang trí particle trong game, sẽ tìm hiểu ở các bài sau

Download


Ở bài 3 chúng ta sẽ học cách di chuyển các Sushi, khi đó việc ăn Sushi, hiệu ứng nổ, rơi Sushi sẽ dễ dàng nhìn thấy hơn. Bài này chỉ là bài chuẩn bị cho bài sau thôi mà.

Chào và hẹn gặp lại ở bài sau


20 comments:

  1. bai hay qua cam on ban nhe mong ban co them nhieu chia se them :-)

    ReplyDelete
  2. Khong biet ban da co y tuong gi ve do kho' chua

    ReplyDelete
  3. khong co file resource ban oi @@ hinh nhu up loi

    ReplyDelete
  4. hinh nhu ham rand() trong c++ ko chuan lam thi phai toan ra gia tri giong nhau nua

    ReplyDelete
  5. o ham init cua play layer ban them srand ( time(0) ); thi se ra random nhieu hon.

    ReplyDelete
  6. Mình phát hiện ra khi dùng SpriteBatchNode để lưu các Sprite Sushi thì ở win32 và mobile mạnh thì ko sao, khi dùng con galaxy y thì nó ko lưu hết các sushi được, chỉ hiển thị 29 cái. Khi đó mình dùng layer->addchild để add sushi. Đến khi remove mình set sushi=null được không nhỉ, liệu có làm tăng bộ nhớ ko?

    ReplyDelete
    Replies
    1. Man hinh Galaxy Y be ti teo ma, trong bai nay set cho man 3 Inch thi phai,

      Delete
  7. mình căn cho hiển thị đầy đủ rồi, nhưng khởi tạo bao giờ cũng chỉ được 29 con với ma trận 6x6. ăn vài con thì lại hiển thị đủ 36 con, dùng layer->addChild thì ko bị như thế.

    ReplyDelete
    Replies
    1. Ô thật thế à? Mình cũng có điều kiện để Test nhiều máy, có mỗi con LTE2 ghẻ đang cho đi ở vì hỏng nguồn. hix. Bạn đã làm tới phần cho Di chuyển Sushi rồi à? Cứ thử chơi lâu lâu xem máy có giật không, xem ram có tăng lên không?

      SpriteBatchNode xử lý nhiều Sprite tốt hơn chứ nhỉ?

      Delete
    2. mình test trên con galaxy y thấy initMatrix bao giờ cũng chỉ hiện được 29 con, sau khi move vài con thì hiện đủ rồi sau đó lại khuyết mất 7 con, nhưng test trên g pro hay win32 thì ko bị. Mình làm phần move rồi nhưng cũng còn lỗi. Mình dùng layer->addChild sau đó khi ăn sushi thì set m_matrix[] = null thôi chứ ko removeChild. Mình chơi trong khoảng 5p thấy ram cũng khá ổn định (mới test win32). Còn giật hay ko thì chưa test được vì phần cho sushi rớt tớ sửa lại đang bị giật kể cả khi dùng SpriteBatchNode

      Delete
  8. Mình thực hiện theo hướng dẫn của bài nhưng khi chạy nếu ăn sushi thì tất cả sushi trên màn hình biến mất hết, có ai bị như mình không. Như vậy là mình đã thưc hiện đúng hay là source chưa chính xác, check giúp mình với nhé.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Mình cũng bị giống như bạn nhưng trên thiết bị android mới bị với lại ăn sushi thường thì ok còn ăn sushi đặc biệt thì sau khi ăn các sushi mới biến mất luôn, còn trên win32 vẫn ok không hiểu sao? Bạn có cách giải quyết chưa?

      Delete
    3. Vì là resource mẫu, được chia sẻ trên mạng nên chắc chắn còn nhiều lỗi không mong muốn các bạn nhé. Nguyên nhân lỗi thì có nhiều, do thuật toán chẳng hạn, hoặc do bug của engine với 1 thành phần nào đó.

      Các bạn đừng quá chán nản, đây chỉ là 1 bài tập làm game mà thôi, có thể có nhiều bug không giải quyết hết được ( vì giải quyết hết được thì làm thành game publish luôn cho rồi ). Và nhiều khi cũng lười sửa lỗi. Quan trọng là chúng ta nắm được tin thần của bài mẫu, nắm được cách vận dụng các thuật toán, giải quyết vấn đề, cách sử dụng engine cho tốt, phải ko?

      Delete
  9. mình bị lỗi file CCParticleSystem.cpp là sao vậy bạn?

    ReplyDelete
    Replies
    1. sorry mình gõ sai tên file stars.plist thành start.plist @@

      Delete
  10. Cảm ơn chủ thớt.
    Sau mấy tháng làm viêc. Đã có game:
    https://play.google.com/store/apps/details?id=com.yellow.ant.FunnyJewel

    ReplyDelete