Saturday, May 31, 2014

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

Hi mọi người!

Vậy là chúng ta đã cùng nhau làm xong 2 Game đơn giản ở những bài trước. Trong bài này mình sẽ hướng dẫn các bạn làm 1 game khó hơn, giống như là Candy Crush, và Bejewer nhé.

À tiện đây mình cũng muốn nói đôi lời. Những ai đang có ý định kiếm mấy game người khác đã chia sẻ code hoặc hướng dẫn làm trên mạng rồi Clone lại, chỉnh sửa tí chút sau đó add quảng cáo rồi tung lên Store để kiếm tiền thì mình có lời như này: XIN ĐỪNG LÀM RÁC các kho ứng dụng theo cách như vậy.

Hãy học làm game một cách chuyên nghiệp, suy nghĩ và sáng tạo ra sản phẩm của riêng mình, mới lạ độc đáo thì sẽ được người dùng đón nhận thôi chứ đừng Clone, nhái, ăn theo => RÁC lắm. Nếu có nhái, ăn theo, thì hãy làm cho nó mới hơn, hay hơn thì hãy làm. Còn không thì tự suy nghĩ và sáng tạo ra 1 game của riêng mình ấy, dù có dở cũng không ai chửi bạn. Chứ đã nhái lại mà còn dở thì..... ăn GẠCH nhá.

Chúng ta bắt đầu bài học thôi.

Để chuẩn bị cho bài học, các bạn cần ôn lại 1 chút kiến thức C++ về mảng 1-2 chiều, con trỏ cấp 1 (*pointer) và con trỏ cấp 2(**pointer), Mối quan hệ giữa con trỏ và mảng nhé, khá quan trọng đó

Trong bài này, chúng ta cần làm các công việc sau đây:

+ Xây dựng Class Sushi, => tạo ra các miếng sushi ở vị trí hàng cột ( thuộc ma trận )
+ Xây dựng màn chơi bằng cách tạo 1 ma trận ( mảng 2 chiều ) chứa các Sushi.
+ Tạo và làm rơi Sushi xuống khi khởi đầu màn chơi

Nhiệm vụ chỉ có vậy thôi. Chúng ta Go nhé

>cocos new sushi -p com.vn.sushi -l cpp -d f:android/project

B1 - Xây dựng Class Sushi

Mở Class của Project Sushi vừa tạo, mở file AppDelegate.cpp sửa và thêm 1 số lệnh sau

+ Phần include thay HelloWorldScene.h = PlayLayer.h, đơn giản chỉ là ko dùng tên HelloWorld nữa, nghe Amatuer làm sao
+ Thêm đoạn code sau vào trước lệnh director->setDisplayStats(true);

   // Thiết lập độ phân giải
   glview->setDesignResolutionSize(320, 480, ResolutionPolicy::FIXED_WIDTH);

   // Thiết lập đường dẫn tới thư mục w640 trong Resource khi biên dịch
    std::vector<std::string> searchPath;
    searchPath.push_back("w640");
    CCFileUtils::getInstance()->setSearchPaths(searchPath);
    director->setContentScaleFactor(640 / 320);

+ Sửa lệnh  auto scene = HelloWorldScene::createScene(); thành auto scene = PlayLayer::createScene();

Bạn xóa 2 file HelloWorldScene.h và .cpp đi nhé vì bài này chúng ta ko dùng đến nữa, mà tạo hẳn file mới và các lớp mới.

+ Bạn tạo 4 file sau ( file rỗng)
- PlayLayer.h, .cpp
- SushiSprite.h, .cpp

+ Đồng thời mở file sushi.vcxproj ( trong thư mục proj.win32) và add vào 4 file trên nhé. Cách add bạn search HelloWorldScene là biết cách. ( Và phải xóa HelloWorldScene ở trong này luôn nhé ).

Dựng Class Sushi

Mở file SushiSprite.h, chèn vào đoạn lệnh sau


#ifndef __SushiSprite_H__
#define __SushiSprite_H__

#include "cocos2d.h"

USING_NS_CC;

class SushiSprite:public Sprite // Kế thừa từ lớp Sprite nhé

{
public:

static SushiSprite* create(int row, int col); // Tạo 1 Sushi tại vị trí hàng, cột thuộc Ma trận
static float getContentWidth(); // Lấy chiều rộng của sprite sushi, cần thiết cho việc tính toán về sau
CC_SYNTHESIZE(int,m_row,Row); // Vị trí hàng của Sushi trong Ma trận
CC_SYNTHESIZE(int,m_col,Col);  // Vị trí hàng của Sushi trong Ma trận
CC_SYNTHESIZE(int,m_imgIndex,ImgIndex); // Loại Sushi
};

#endif

CC_SYNTHESIZE thì ở các bài trước mình đã giải thích rồi nhé, ko quá khó để hiểu đâu. Hãy dùng ô Search của Blog và search "CC_SYNTHESIZE_READONLY" nhé ra ngay bài 12

File Sushi.cpp, thêm vào 

#include "SushiSprite.h"

USING_NS_CC;

#define TOTAL_SUSHI 6 // Tổng số loại Sushi

// Tạo 1 mảng con trỏ, mỗi con trỏ trỏ tới 1 chuỗi, sushiNormal[i] lưu địa chỉ chuỗi i
static const char *sushiNormal[TOTAL_SUSHI] = {
"sushi_1n.png",
"sushi_2n.png",
"sushi_3n.png",
"sushi_4n.png",
"sushi_5n.png",
 "sushi_6n.png"
};

// Lấy chiều rộng của đối tượng 
float SushiSprite::getContentWidth()
{
    static float itemWidth = 0;
    if (itemWidth==0) {

// Tạo ra 1 sushi từ mảng trên
        auto sprite = Sprite::createWithSpriteFrameName(sushiNormal[0]);
        itemWidth = sprite->getContentSize().width;
    }
    return itemWidth;
}

// Tạo mới 1 Sushi có vị trí row, col, trả về 1 con trỏ kiếu SushiSprite*
SushiSprite *SushiSprite::create(int row, int col)
{
// Tạo mới
SushiSprite *sushi = new SushiSprite();
// Gắn hàng, cột, index
sushi->m_row = row;
sushi->m_col = col;
sushi->m_imgIndex =  rand() % TOTAL_SUSHI; // random loại Sushi từ 0-5 (= index của mảng)
// Tạo hình ảnh từ chuỗi của mảng trên
sushi->initWithSpriteFrameName(sushiNormal[sushi->m_imgIndex]);
sushi->autorelease(); // Tự động hủy khi cần
return sushi;
}

B2 - Xây dựng Màn chơi, tạo hiệu ứng rơi Sushi

Ở bước trên chúng ta đã xây dựng xong Class Sushi, vậy làm thế nào để tạo ra màn chơi với 1 ma trận hàng +cột chứa sushi đây?. Các bạn theo dõi bên dưới nhé

+ Mở file PlayLayer.h ( đang trống) Code file như sau

#ifndef __PlayLayer_H__
#define __PlayLayer_H__

#include "cocos2d.h"

USING_NS_CC;

class SushiSprite; // Chỗ này bạn có thể cho lên #include

class PlayLayer : public Layer
{
public:

    PlayLayer();
    ~PlayLayer();
    static Scene* createScene(); // Tạo màn chơi
    CREATE_FUNC(PlayLayer);
    // Khởi tạo
    virtual bool init() override;
private:

    // Sprite Sheet để lưu các loạt ảnh tạo animation, học ở bài 19
    SpriteBatchNode *spriteSheet;

    // Ma trận 2 chiều dùng con trỏ cấp 2 để lưu SushiSprite* ( Hãy đọc lại phần con trỏ và mảng 2 chiều) 
    SushiSprite **m_matrix;

    // Kích thước Ma trận, hàng, cột
    int m_width;
    int m_height;

    // Vị trí căn chỉnh trên màn hình ( Tọa độ Left Bottom)
    float m_matrixLeftBottomX;
    float m_matrixLeftBottomY;
    
    // Hàm tạo ma trận
    void initMatrix();

    // Tạo Sushi và cho rơi xuống ở vị trí hàng cột bất kỳ
    void createAndDropSushi(int row, int col);

    // Trả lại vị trí tọa độ Point của Sushi tại vị trí hàng + cột trong ma trận
    Point positionOfItem(int row, int col);
};

#endif /* defined(__PlayLayer_H__) */

+ Mở file PlayLayer.cpp để xây dựng các hàm, Code file như sau ( Thực sự là hơi dài nếu tính cả Comment của mình ). Cũng đành copy vậy

#include "PlayLayer.h"
#include "SushiSprite.h"

// Định nghĩa kích thước ma trận 6x8
#define MATRIX_WIDTH (6)
#define MATRIX_HEIGHT (8)

// Khoảng cách giữa cách ảnh Sushi = 1
#define SUSHI_GAP (1)

// Hàm tạo Contructor, tất cả con trỏ = NULL, giá trị =0

PlayLayer::PlayLayer()
: spriteSheet(NULL) //chỗ này là dấu 2 chấm, đằng sau dấu phẩy hết nha
, m_matrix(NULL)
, m_width(0)
, m_height(0)
, m_matrixLeftBottomX(0)
, m_matrixLeftBottomY(0)
{
}

// Hàm hủy thì giải phóng con trỏ

PlayLayer::~PlayLayer()
{
    if (m_matrix) {
        free(m_matrix);
    }
}

// Hàm tạo Scene, đơn giản quá

Scene *PlayLayer::createScene()
{
    auto scene = Scene::create();
    auto layer = PlayLayer::create();
    scene->addChild(layer);
    return scene;
}


// Hàm khởi tạo init()
bool PlayLayer::init()
{
    if (!Layer::init()) {
        return false;
    }
 
    //Tạo ảnh nền
    Size winSize = Director::getInstance()->getWinSize();
    auto background = Sprite::create("background.png");

    // Điểm neo, điểm này sẽ ảnh hưởng tới việc đặt setPosition của sprite, nếu ko đặt điểm neo thì khi setPosition sẽ mặc định lấy điểm trung tâm của Sprite đặt lên màn hình 
    background->setAnchorPoint(Point(0, 1));
    background->setPosition(Point(0, winSize.height)); // Điểm neo như trên dễ đặt Position hơn nhỉ
    this->addChild(background);

 
    // Khởi tạo bộ đệm Sprite Frame
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("sushi.plist");
    spriteSheet = SpriteBatchNode::create("sushi.pvr.ccz"); // Chú ý hàm này, "pvr.ccz" tập tin đã nén và hõa hóa = TexturePacker, chứa hình ảnh, bạn có thể xem chúng bằng phần mềm TexturePacker, tool PVR View

    addChild(spriteSheet); // Thêm SpriteSheet vào Layer
 
// Kích thước ma trận, 
    m_width = MATRIX_WIDTH; // =6
    m_height = MATRIX_HEIGHT; //=8
 
    // Đặt vị trí ma trận, tính toán 1 chút là ra ấy mà, lấy tổng kích thước màn hình, trừ đi các khoảng cách sẽ ra 2 khoảng bên trái và phải của Ma trận

    m_matrixLeftBottomX = (winSize.width - SushiSprite::getContentWidth() * m_width - (m_width - 1) * SUSHI_GAP) / 2;
    m_matrixLeftBottomY = (winSize.height - SushiSprite::getContentWidth() * m_height - (m_height - 1) * SUSHI_GAP) / 2;
 
 // Khởi tạo 1 mảng

// Kích thước bộ nhớ arraySize = sizeof (kiểu) x kích thước mảng

    int arraySize = sizeof(SushiSprite *) * m_width * m_height;

// Cấp phát bộ nhớ bằng hàm malloc, ( xem lại cách sử dụng hàm này ), ép kiểu về kiểu của biến SushiSprite **, rồi cấp phát với kích thước arraySize 
    m_matrix = (SushiSprite **)malloc(arraySize);
    memset((void*)m_matrix, 0, arraySize); // Đặt tất cả giá trị của mảng là 0, bắt buộc ép kiểu void* của mọi loại mảng
 
    initMatrix(); // Khởi tạo ma trận Sushi
    return true;
}

void PlayLayer::initMatrix()
{

// Duyệt các phần tử ma trận 2 chiều
    for (int row = 0; row < m_height; row++) {
for (int col = 0; col < m_width; col++) {
            createAndDropSushi(row, col); // Tạo và làm rơi Sushi xuống vị trí hàng + cột
        }
    }
}

void PlayLayer::createAndDropSushi(int row, int col)
{
    Size size = Director::getInstance()->getWinSize();
 
    SushiSprite *sushi = SushiSprite::create(row, col); // Gọi đến hàm tạo ra Sushi của lớp SushiSprite
 
    // Tạo animation, or Action?
    Point endPosition = positionOfItem(row, col); // Lấy tọa độ Point từ row, col truyền vào
    Point startPosition = Point(endPosition.x, endPosition.y + size.height / 2); // (y) Điểm đầu = Điểm Cuối + 1 khoảng nửa màn hình
    sushi->setPosition(startPosition);

    float speed = startPosition.y / (2 * size.height); // tốc độ

    sushi->runAction(MoveTo::create(speed, endPosition)); // Di chuyển rơi xuống

    // Thêm vào Spritesheet
    spriteSheet->addChild(sushi);


// Thêm sushi vào mảng, chỗ này là cách quy mảng 2 chiều về mảng 1 chiều nhé, a[i][j] = a[i*COL + j]

    m_matrix[row * m_width + col] = sushi;
}

// Tọa độ Point từ vị trí row, col
Point PlayLayer::positionOfItem(int row, int col)
{
    float x = m_matrixLeftBottomX + (SushiSprite::getContentWidth() + SUSHI_GAP) * col + SushiSprite::getContentWidth() / 2;
    float y = m_matrixLeftBottomY + (SushiSprite::getContentWidth() + SUSHI_GAP) * row + SushiSprite::getContentWidth() / 2;
    return Point(x, y);
}

Xong phần Code, hãy build và chạy thử xem thế nào?



Ngon rồi

Tổng kết lại ở bài này chúng ta học được :
+ Tạo Class mới ( Sushi ) đơn giản bằng C++
+ Ôn lại kiến thức về mảng, ma trận cấp 2, con trỏ đơn, con trỏ 2 cấp
+ Tạo ma trận Sushi
+ Tính toán 1 chút về cách đặt ma trận trên màn hình
+ Tạo Action rơi các sushi xuống ở màn chơi

* Lưu ý 1 chút:

1/ Bạn thấy rằng hình như các sushu + background có vẻ tràn ra màn hình, kích thước không được hợp lý lắm. Thì phải thôi, nguyên nhân khiến bị như thế này là

+ Trong file AppDelegate.cpp, ta chỉ setting chỉnh kích thước trên mobile thôi, chứ ko phải cho window

Các bạn thử chạy trên máy thật hoặc máy ảo xem như nào. ĐT mình mới chêt nguồn rồi. và máy ảo thì nặng quá, và thường là chạy không đúng, hay force stop.

2/ Có vẻ hàm rand() không hiệu quả ( trên win) thì phải, chạy đi chạy lại, thì loại Sushi vẫn thế. Không biết trên máy ĐT thế nào?

Mình kết thúc bài này ở đây nhé.

Download:

+ Class
+ Resource
+ Texture, mở bằng TexturePacker ( hướng dẫn )

Chào và hẹn gặp lại các bạn trong những bài sau

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

13 comments:

  1. Ohm đã có bài mới rùi thanks

    ReplyDelete
  2. cám ơn bạn nhiều. Bạn up thêm folder resource lên nhé

    ReplyDelete
  3. Mình ko hiểu lắm trong file PlayLayer.h có dòng "class SushiSprite;" các bạn giải thích dùm cho mình nhé :)

    ReplyDelete
    Replies
    1. Trong bài mình có nói rồi mà, đặt class SushiSprite; ở file này cũng giống như việc bạn đặt #include"SushiSprite.h" thôi

      Thậm chí bạn có thể khai báo cả 1 lớp SushiSprite ở trong file PlayLayer.h, thậm chí nhiều Class hơn, nhưng sẽ rối mắt.

      Kiến thức C+

      Delete
    2. Mình thường dùng #include nhiều, ít dùng kiểu này nên thấy lạ. Khi dùng #include thì nó sẽ chép toàn bộ đoạn code vào vi trí đó. Thanks bạn đã chia sẻ.

      Delete
  4. bạn thêm srand(time(NULL)); vào thì rand() sẽ thay đổi

    ReplyDelete
    Replies
    1. thêm dòng này vào đâu bạn, mình thêm vào hàm createsushi thì nó chỉ ra 1 loại thôi

      Delete
  5. Cho em hỏi alfm sao để thay đổi dổi file susi.plist để vào game hiện hình ảnh khác được ạ

    ReplyDelete
  6. Thank you.
    Sau mấy tháng làm viêc. Đã có game:
    https://play.google.com/store/apps/details?id=com.yellow.ant.FunnyJewel

    ReplyDelete