Cocos2d-x 3.2 Final

Cập nhật lên phiên bản Cocos2d-x 3.2. Bổ sung 3D sprite, 3D animation, game controller, fast Tilemap,..v...v... Cập nhật thường xuyên nhé

Cocos2d-x 3.0 Final

Cocos2d-x là Engine lập trình game đa nền tảng phổ biến nhất trên thế giới. Tính năng mạnh mẽ, dễ sử dụng, miễn phí, mã nguồn mở.

Cocos2d-x V3.0 Final

Hướng dẫn chi tiết, đầy đủ bằng Video, Blog. Cộng đồng hỗ trợ rộng lớn. Sản phẩm phong phú mọi nền tảng

Game Badland

Một trong những game nổi tiếng nhất được tạo ra bởi Engine Cocos2d-x.

Cocos2d-x V3.1

Cập nhật phiên bản Engine Cocos2d-x V3.1 bổ sung thêm nhiều tính năng. Sửa các lỗi nhỏ của các phiên bản trước

Contra Evolution - KONAMI

1 Game rất hay không thể bỏ qua, 1 vé trở về với tuổi thơ.

Wednesday, July 16, 2014

Hướng dẫn cách Port code từ Cocos2d-x 2 sang Cocos2d-x 3.x ( Update liên tục ...)

Hi mọi người!

Lâu lâu lười viết bài thế, không phải là không còn gì để viết, không còn nguồn để tham khảo. Mà là vì những bài code về sau dung lượng code lớn quá, up lên Blog khá là mất thời gian vì diễn giải khá dài, dù có chia nhỏ từng phần ra cũng hơi oải các bạn ạ. Thời gian để viết Blog cũng đủ cho mình chiến được 1 project tầm trung rồi, vì thế nên các bạn hết sức thông cảm nếu mình có ra bài chậm.

Vậy giải pháp cho vấn đề này như thế nào? Mình xin đưa ra với các bạn 1 vài giải pháp như sau

1/ Nếu bài viết trên Blog, vẫn chia Part 1-2-3 cho tiện, và mình sẽ Copy + Paste chỉ giải thích ( comment ) những đoạn code, phương pháp nào thật sự khó hiểu, hoặc mới chưa từng có trên Blog. Còn lại các bạn nghiên cứu trong từng file .h, .cpp mình sẽ up lên. Trong code cũng chỉ comment đoạn quan trọng, kẻo rối mắt nhé

2/ Code cho phiên bản V3 ngày càng nhiều lên, các bạn có thể tìm trên Github.com bằng các key thích hợp. Tuy nhiên chúng ta không thể bỏ qua 1 nguồn tài nguyên khá lớn từ phiên bản Engine Cocos2dx -2.x được. Sẽ có bạn bảo, code đó làm sao chạy được với Engine 3.x, tất nhiên rồi ( Code 3.1 nhiều khi còn chả chạy được trên V3.2 đó bạn ). Đó là lý do mình viết bài này, chúng ta sẽ Port code từ phiên bản cũ lên phiên bản 3.x rồi Biên dịch = phiên bản mới. Cũng hay đấy chứ.

Lợi ích của việc Port Code này là gì? Theo mình thì có 1 vài lợi ích sau

+ Mở rộng nguồn tài nguyên, chúng ta sẽ có nhiều dự án để thực hành hơn
+ Mã nguồn cũng phong phú hơn do 2.x ra khá lâu rồi
+ Ôn lại được những kiến thức đã học của 3.x. Giúp bạn nắm chắc hơn về cú pháp lệnh của 3.x, sướng nhé
+ Học thêm được một số giải thuật, phương pháp giải quyết bài toán, cấu trúc chương trình mà những người đi trước đã share ( nhiều bộ code full nhé ).
+ Còn gì nữa không nhỉ? tạm thời thế đã :-))

Những ai có thể Port Code?

Những người đã đọc qua 40 bài viết ( có 29 bài học và mấy chú ý ) trong blog này hoàn toàn có thể làm được nhé. Bạn không tự tin ư? Với chính bản thân mình lúc đầu khi bắt tay vào Port code cũng cảm thấy hơi e dè, nhưng chỉ cần qua 1 project thôi là bạn sẽ cảm thấy tự tin lên nhiều. Mình khẳng định chắc chắn đấy.

Như vậy nhé, mình khẳng định lại lần cuối, mọi người hoàn toàn có thể làm được theo những lưu ý dưới đây
( Bài viết sẽ được bổ sung dần dần nhé )

PORT CODE

B1 - Tạo mới Project bằng Engine 3.x, dễ như ăn kẹo

B2 - Không nên copy Class của 2.x đập thẳng vào Class 3.x rồi sửa, rồi build nhé, làm vậy sẽ nhanh chóng bị rối vì rất nhiều lỗi mà ko biết vì sao.

B3 - Ngó nghiêng file AppDelegate.cpp, .h của 2 phiên bản, có đôi chút khác ở phần glview, và phần định hướng Resource, và hàm run scene đầu tiên bạn chỉnh lại 1 chút là được. Phần này ko quá quan trọng, bạn có thể để nguyên của phiên bản 3.x nhé. Có gì mình fix sau

B4 - Trong thư mục Class của 2.x, Tìm 2 file Quan trọng nhất của game - có nhiệm vụ quản lý toàn bộ game, đó là 2 file sẽ được chạy đầu tiên, bạn chỉ cần nhìn trong AppDelegate.cpp là thấy được scene đầu tiên được chạy là của lớp nào thì lớp đó sẽ nằm trong 2 file kia. Thường là thế, không tính đến những cấu trúc file loằng ngoằng do sở thích của người lập trình.

B5 - Trong 2 file quan trọng nhất này, bạn sẽ thấy rất nhiều #Inlude từ các file khác, rất nhiều thuộc tính, nhiều hàm, rối tinh rối mù đúng không. Hãy xem qua để biết được cấu trúc liên kết của các lớp, các hàm trong Project là như nào. Rối quá thì dùng giấy ghi lại thôi.

B6 - Bắt đầu làm việc. Trong Class của 3.x, bạn mở file HelloWorldScene.h, nó là file quan trọng đầu tiên đấy. Bắt đầu copy các thuộc tính, các hàm từ bên file .h quan trọng nhất bên 2.x sang nhé ( cứ copy y nguyên nhé, phần sửa đổi cú pháp lệnh, hàm để dành Bước cuối cùng ).

Sau đó trong file HelloWorldScene.cpp,  bạn tạo ra các khai báo hàm rỗng - kế cả hàm hủy và hàm tạo. Rồi build thử, nếu chạy được nghĩa là OK, nếu không được, lỗi có thể do :

+ Lỗi cú pháp, thư viện hàm đã "lỗi thời". Cách sửa trình bày sau
+ Code bị lỗi do thừa thiếu, hoặc có sử dụng đối tượng của lớp khác mà ko #include, hãy comment nó lại để tránh lỗi đã.
+ Lỗi không giải thích nổi, do VS ( mình toàn xài VS ) báo rất khó hiểu, nên phải dựa vào kinh nghiệm để phán đoán và sửa lỗi. Với 3-4 tháng kinh nghiệm là đủ dùng.

B7 - Khi build được thành công với cặp file này ( .h, .cpp) nghĩa là file .h đã OK ( có thể lỗi về cú pháp thì bạn nhảy đến bước cuối cùng - xem rồi sửa theo ) file .cpp thì toàn hàm rỗng OK là đúng rồi. Giờ làm công việc khó khăn hơn là, bắt đầu xử lý các hàm rỗng đó nhé ( Trong Class 3.x)

+ Phần hàm tạo và hàm hủy, khá dễ. Hàm tạo thì khởi tạo giá trị cho các đối tượng, nếu là con trỏ thì cho nó là Null. Hàm hủy thì giải phóng các biến con trỏ ( những biến mà trong project có sử dụng hàm new nhé )

+ Hàm createScene() đển nguyên, trừ phi bên 2.x có thêm cú pháp khác
2.x nó thế này
CCScene* GameLayer::scene()
{
    CCScene * scene = NULL;
    do 
    {
    } while (0);
    return scene;
// Thường ko copy gì cả nhé

+ Hàm init (), bạn để nguyên như vậy, copy thêm các câu lệnh và hàm nằm trong khối lệnh sau ( trong file 2.x )

bool GameLayer::init() // init của v 2.x
{
   bool bRet = false;
    do 
    {
    CC_BREAK_IF(! CCLayer::init()); // không copy

// Copy các hàm, lệnh ở đoạn này
    bRet = true; // Không copy

    } while (0);
    return bRet;
}

B8 - Rồi, build thử xem có lỗi gì không, khả năng là có lỗi cao, vì trong hàm init sẽ có gọi đến 1 số hàm khác của project, thậm chí gọi tới đối tượng của lớp khác. Bạn hãy comment hết lại những hàm, code phát sinh lỗi. Build cho tới khi thành công thì bắt đầu công đoạn tiếp theo.

B9 - Khi đã build thành công ở bước 8, bạn mở comment từng hàm 1 trong hàm init ( Các hàm này đểu được khai báo rỗng cả rồi nên ko sợ lỗi ) Khi mở hàm nào, hãy copy code từ hàm tương ứng bên 2x chuyển sang, và chú ý xem trong hàm mới copy đó, có gọi tới hàm nào khác không thì bước tiếp theo là copy code của Hàm được gọi đó vào.

Cứ theo từng bước như thế, bạn sẽ mở hết các hàm.

B10 - Trong quá trình mở hàm, bạn sẽ gặp phải các đối tượng của lớp khác, khi này bạn sẽ làm như sau. Copy 2 file .h, .cpp của lớp đó từ 2.x sang 3.x của mình, Nhớ comment lại code, hàm, của 2 file đó rồi thực hiện mở comment như các bước trên. ( Nhớ phải thêm Class mới = VS hoặc Android.MK nhé )

Cứ làm tuần tự như vậy cho đến khi copy mở hết code, hàm từ 2.x sang 3.x. Tất nhiên không chỉ đơn giản như vậy, vì trong code, hàm của 2.x có cú pháp đã lỗi thời không phù hợp với 3.x nên báo lỗi ngay. Vậy thì bạn chỉ việc làm theo các lưu ý của bước cuối cùng sau đây là OK thôi

B11 - Chuyển đổi cú pháp code, cập nhật tham số hàm. QUAN TRỌNG

Vì sang 3.x thư viện có sửa đổi nhiều nên nhiều lớp cũ, hàm cũ không thể dùng được, hãy tham khảo trong bài cpp-test để lấy đúng nguyên mẫu của hàm đó, sửa đổi là xong, Tất cả mình sẽ trình bày bên dưới:

1/ Hầu như các prefix CC đều được loại bỏ khỏi lớp nhé, nêu bạn xóa cái CC này đi cho gọn, thoáng

Ví dụ CCSprite => Sprite  ,v.v...

2/ Thay đổi tên 1 số lớp
Ví dụ cpp, hoặc Point, CCPointMake => Vec2

CCPointZero => Vec2::ZERO

3/ Hàm Touch đơn

Không nhớ 2.x nó thế nào

Chuyển thành

bool onTouchBegan(Touch* touch, Event* event);

4/ Touch đa điểm

2.x void ccTouchesBegan(CCSet* pTouches, CCEvent* event);

3.x void onTouchesBegan(const std::vector<Touch*>& touches, Event *event); 

5/ dạng share

2.x _screenSize = CCDirector::sharedDirector()->getWinSize();

3.x _screenSize = Director::getInstance()->getWinSize();

Đại loại có dạng share ở dầu lệnh static bạn có thể dùng getInstane , hãy chỉ chuột vào lệnh sharedDirector() nó sẽ hiện bảng lệnh nào dùng để thay thế ( VS + VAX nhé )

6/ Hàm callback là dạng hàm được gọi khi thực hiện event nào đó, ví dụ click chuột menu, hoặc trong action sequence hay gặp

2.x (CCObject* pSender)

3.x (Ref* pSender)

7/ Menu
2.x
CCMenuItemSprite * starGametItem = CCMenuItemSprite::create(
                                                                menuItemOff,
                                                                menuItemOn,
                                                                this,
                                                                menu_selector(GameLayer::startGame));
   
3.x
MenuItemSprite * starGametItem = MenuItemSprite::create(
                                                                menuItemOff,
                                                                menuItemOn,
                                                                CC_CALL_BACK_x(GameLayer::startGame,this));
x = 0,1,2,3 tùy thuộc vào số tham số của hàm được gọi

8/ CCArray

Chuyển sang dùng Vector, ví dụ vector<T> vec; hoặc Vector<T> Vec, hai thằng này có vẻ giống nhau nhé, nhưng hàm của chúng có khác nhau đấy.

9/ spriteFrameByName
2.x
CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName()
3.x
SpriteFrameCache::getInstance()->getSpriteFrameByName("")
10/ Create

CCSprite::spriteWithFile() -> Sprite::create();

11/ Action

runAction(CCSequence::actions(CCDelayTime::actionWithDuration(0.5f),
 CCCallFunc::actionWithTarget(this, callfunc_selector(HelloWorld::resetGame)),NULL));

3.x

runAction(Sequence::create(DelayTime::create(0.5f),
  CallFuncN::create(CC_CALL_BACK_x(HelloWorld::resetGame,this)),NULL));
CC_CALL_BACK_x, x  = 0,1,2,3  tùy vào số tham số của resetGame

=> Các action đều chuyển về ::create

12/ CGFloat

=> float
   

13/ Debug Box2D

void draw() - 2.x
virtual void draw(Renderer *renderer, const Mat4 &transform, bool transformUpdated) override; - 3.1

3.2
virtual void draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) override;
Tạm thời như vậy thôi nhỉ, còn 1 vài đoạn code đặc biệt nữa, mình không nhớ hết, vì giờ lục lại các bài code hơi bị nhiều. Có gì mình sẽ bổ sung sau. OK?

Vậy là trong bài này chúng ta đã học được cách để Port code từ phiên bản 2.x sang 3.x mà không quá khó khăn rồi.

Nếu bạn làm theo các bước trên đặc biệt chú ý bước cuối cùng quan trọng thì sẽ không Project 2.x nào làm khó được bạn, Mình đã làm 2-3 project 2.x rồi, thành công cả. Hì

Có thể tóm gọn lại như sau

+ Làm theo từng bước: Sửa cú pháp trước =>Copy file =>đóng - mở comment = > Copy code => đóng - mở comment => Debug => mở từng hàm => Debug => đóng mở comment chỗ lỗi => Debug => KẾT QUẢ

Thế thôi nhỉ, Hẹn gặp lại ở bài sau!

Friday, July 11, 2014

Bài 28 - Box2D - Physics Body cho những vật tĩnh có hình khối phức tạp

Hi, tất cả!

Ở mấy bài trước ( 15-16 gì đó) chúng ta đã cùng nhau nghiên cứu về cách sử dụng Box2D để tạo môi trường vật lý trong Game 2D. Trong bài đó, đối tượng của chúng ta chỉ là một quả bóng hình tròn bán kính R, nên việc đặt body cho nó là quá dễ dàng. Vậy còn đối với những đối tượng có hình dáng bất kỳ thì sao ( chỉ xét những vật tĩnh, ko xét animation nhé) ? Làm thế nào để đặt body cho chúng. Đó sẽ chính là nội dung trong bài hôm nay của chúng ta.

Trong bài này chúng ta sẽ buộc phải sử dụng 2 công cụ PhysicsEditor và TexturePacker ( có thể sử dụng ShoeBox, hoặc SpritePacker để thay thế nếu TexturePacker crack bị lỗi - Win 64 của mình ko bị lỗi sau crack, 32 thì dính chưởng : ((

Đồng thời bạn cũng sẽ được cung cấp 1 bộ thư viện để phân tích file ảnh + file thông số physics được tạo ra bởi 2 phần mềm kể trên. Do đó, hầu như bạn chẳng phải viết code gì nặng nhọc đâu, chỉ việc sử dụng cái thư viện đó để phân tích file nạp vào rồi thiết lập được body ngay. Để hiểu sâu bạn có thể tìm hiểu thư viện đó ( chỉ gồm 1 file .h và .cpp ) sẽ rất có ích nếu bạn có ý định sử dụng Box2D về sau.

Nội dung trong bài gồm các phần sau đây:

+ Sử dụng PhysicsEditor + TexturePacker để thiết kế body cho các đối tượng có hình khối phức tạp
+ Nạp vào game
+ Phương pháp Debug - vẽ khung của đối tượng 1 cách trực quan
+ Test thử

OK, ngắn gọn như vậy thôi, triển nào

À, bài này mình tìm được trên trang codeandweb - trang chủ của PhysicsEditor và TexturePacker

Đây https://www.codeandweb.com/physicseditor/tutorials

Nhưng chủ yếu là Cocos2D, và các Engine khác ( phân biệt đối xử thật ). Lúc đầu cũng khá hì hụi để tìm code trên Github, nhưng khá cũ. Đành sửa và port sang bản 3.x vậy. Các bạn đọc hết 27 bài trước đây trong Blog này thì mình tin là việc port Code từ 2.x sang 3.x thật dễ dàng. Mình muốn chia sẻ 1 điều là, khi tự mình nghiên cứu tìm tòi cách để làm 1 cái gì đó thì kết quả đạt được sẽ rất ý nghĩa, vui và nhớ lâu hơn.

À, các bạn nhớ ôn lại:
+ Cách Import Box2D ( Cũng như các thư viện khác ) ở bài 15, 23,24
+ Cách khai báo Class mới, cũng ở các bài trên
+ Hãy đọc bài chú ý khi biên dịch cho cả Win + Android ( Side bar bên phải có dấu tam giác to đùng )

B1 - Sử dụng PhysicsEditor + TexturePacker 

TexturePacker 

Phầm mềm này rất dễ sử dụng, chức năng chính của nó là đóng gói ( pack ) nhiều ảnh đơn thành 1 bức ảnh to + 1 file thông số đi kèm. File ảnh to xuất ra có thể là png hoặc các dạng nén khác. Mục đích là để giảm tải bộ nhớ, dung lượng game, vậy thôi. Đơn giản vô cùng.

Mở chương trình lên, để các thông số mặc định không sửa đổi gì, import ảnh vào thông qua nút + SPrite hoặc + thư mục, trỏ tới THƯ MỤC ẢNH chứa các ảnh sử dụng trong chương trình này. Nhấn Publish, được 2 file, quăng vào Resource của project. Phần này khá đơn giản nên mình ko minh họa bằng hình ảnh nhé.

PhysicsEditor

Mở phần mềm lên, Import hình ảnh vào thông qua nút Open hoặc nút AddSprites, và cũng trỏ  vào THƯ MỤC ẢNH ở trên ( quan trọng nhé ).




Bây giờ bạn để ý tới 3 khu vực quan trọng nhất
+ Trái : là các hình ảnh bạn import vào
+ Giữa là khu vực bạn sẽ vẽ, hoặc auto tracer shape cho hình ảnh thông qua các hình tròn, đa giác, nút auto, flip ngay bên dưới nút AddSprites
+ Phải: Thiết lập chế độ xuất ra - Exporter ( Box2D, Engine xx) và các thông số vật lý cơ bản của Box2D ( dynamic, friction, density, bitmask, v..v...). Bạn chọn mục Exporter là Box2D generic (PLIST) nhé

Lưu ý 1 chút: để tạo shape dạng Polygon, bạn chọn công cụ đa giác, sẽ hiện 1 tam giác đỏ, để thêm đỉnh mới bạn click đúp, rồi kéo các đỉnh khớp với đối tượng

Thực hành luôn, bạn nhấn vào nút Shape Tracer ( nút thứ 3, sau hình đa giác ) nhé sẽ hiện lên bảng Tracer

Bạn để các thông số như hình





 Để ý là
+ Thông số Telerance càng cao thì Vertexer càng nhỏ và Shape càng thiếu chính xác, với hình ko quá phức  tạp bạn để Vertexes vừa phải thôi nhé. Chỉnh sao sao cái Shape tạo ra ( màu đỏ khớp tương đối với hình ảnh của chúng ta là được).

Sau khi tạo Shape xong cho các ảnh trong này, bạn nhấp chuột vào nút Publish, đặt tên file và chọn nơi lưu là Resource của project

Tuy 2file Plist tạo bởi Physics Editor và TexturePacker khá giống nhau nhưng nội dung chúng lưu khác nhau nhé: 1 thằng lưu thông số hình ảnh, 1 thằng lưu thông số hình khối. Bạn mở 2 file lên để so sách sự khác biệt. Và Physics Editor chỉ tạo ra 1 file Plist, còn TexturePacker tạo ra 2 file là: Plist + ảnh Png ( hoặc dạng khác tùy mình chọn ).

B2 - Nạp vào game + Debug drawing Box2D

Ở bước này, bạn tạo 1 project, giả sử tên là newbox2d chẳng hạn, nhớ Import thư viện Box2D vào theo bài 15, 23,24

Mở thư mục cpp-tests trong thư mục Engine  lên, copy các file sau vào Class Project của chúng ta:

+ VisibleRect.h, .cpp theo đường dẫn Q:\ANDROID\Cocos2dx3\tests\cpp-tests\Classes
+ GLES-Render.h, .cpp theo đường dẫn Q:\ANDROID\Cocos2dx3\tests\cpp-tests\Classes\Box2DTestBed
Download và copy 2 file này vào thư mục Class (Tất cả down ở cuối bài)

OK, các bạn không phải làm gì với các file ở trên, vì mình sử dụng chúng làm thư viện hỗ trợ thôi, các bạn thích thì có thể ngó nghiêng qua, không thì thôi, vào phần chính luôn, đó là file HelloWorldScene.h và .cpp

À, các bạn nhớ thêm các class trên vào file. newbox2d.vcxproj trong proj.win32 ( nếu build win )

hoặc trong file Android.MK ( nếu build Apk )

OK, giờ thì tập trung vào file HelloWorldScene.h

Các bạn mở lên, Paste full code sau vào

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

// Nhớ include Box2D, và các mở rộng
#include "cocos2d.h"
#include "Box2D/Box2D.h"
#include "GLES-Render.h"
#include "GB2ShapeCache-x.h"
#include "VisibleRect.h"

USING_NS_CC;

class HelloWorld : public cocos2d::Layer
{
public:

    static cocos2d::Scene* createScene();

    virtual bool init();  
    
    void menuCloseCallback(cocos2d::Ref* pSender);
void addNewSpriteWithCoords(Point p); // Tạo đối tượng tại Point xác định
// Touch, nên để cả 3 hàm, mặc dù chỉ dùng 1 hàm thôi
bool onTouchBegan(Touch* touch, Event* event);
void onTouchMoved(Touch* touch, Event* event);
void onTouchEnded(Touch* touch, Event* event);

// Hàm vẽ để Debug khung body của đối tượng, lưu ý: Hàm này chỉ đúng với Engine 3.0,3.1.x nhé, Ai dùng Engine 3.2 RC0 trở lên ( ko xét alpha,beta) thì phải sửa thành.

// virtual void draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) override;

// Kẻo lỗi tùm lum

virtual void draw(Renderer *renderer, const Mat4 &transform, bool transformUpdated) override;

void update(float dt);
    
    CREATE_FUNC(HelloWorld);

private:

// World và đối tượng Debug Draw
b2World* world;
GLESDebugDraw *m_debugDraw;
};

#endif // __HELLOWORLD_SCENE_H__

Giờ thì tới lượt file HelloWorldScene.cpp

Phần Include, thêm code sau

USING_NS_CC;
using namespace std;
 // Nếu ko dùng namespace này, khai báo string str sẽ lỗi
#define PTM_RATIO 32  

Hàm init()

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
// Lớp Point đã được thay = Vec2 từ V3.1
    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
  
setTouchEnabled( true );
Size screenSize = Director::getInstance()->getWinSize();

// Khởi tạo world của Box2D
b2Vec2 gravity;
gravity.Set(0.0f, -10.0f);
bool doSleep = true;  
world = new b2World(gravity);
world->SetAllowSleeping(doSleep);    
world->SetContinuousPhysics(true);

//-----------DEBUG------------và phải có hàm Draw ở phía dưới nữa
// Debug khung body
m_debugDraw = new GLESDebugDraw( PTM_RATIO );
world->SetDebugDraw(m_debugDraw);

// Các tham số để vẽ, bạn mở dần từng comment ra để thấy sự thay đổi
uint32 flags = 0;
flags += b2Draw::e_shapeBit;
//flags += b2Draw::e_jointBit;
//flags += b2Draw::e_aabbBit;
// flags += b2Draw::e_pairBit;
// flags += b2Draw::e_centerOfMassBit;
m_debugDraw->SetFlags(flags);
//-------------End Debug------------

//Tạo Ground bao quanh màn hình
b2BodyDef groundBodyDef;


groundBodyDef.position.Set(screenSize.width/2/PTM_RATIO, 
screenSize.height/2/PTM_RATIO); 

b2Body* groundBody = world->CreateBody(&groundBodyDef);

b2PolygonShape groundBox;

// bottom
groundBox.SetAsBox(screenSize.width/2/PTM_RATIO, 0, b2Vec2(0, -screenSize.height/2/PTM_RATIO), 0);
groundBody->CreateFixture(&groundBox, 0);

// top
groundBox.SetAsBox(screenSize.width/2/PTM_RATIO, 0, b2Vec2(0, screenSize.height/2/PTM_RATIO), 0);
groundBody->CreateFixture(&groundBox, 0);

// left
groundBox.SetAsBox(0, screenSize.height/2/PTM_RATIO, b2Vec2(-screenSize.width/2/PTM_RATIO, 0), 0);
groundBody->CreateFixture(&groundBox, 0);

// right
groundBox.SetAsBox(0, screenSize.height/2/PTM_RATIO, b2Vec2(screenSize.width/2/PTM_RATIO, 0), 0);
groundBody->CreateFixture(&groundBox, 0);

//---------------------------------------

// Nạp vào game file hình ảnh đóng gói và thông số của nó tạo bởi TexturePacker
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("INFOR.plist");
auto spriteSheet = SpriteBatchNode::create("INFOR.png");
this->addChild(spriteSheet);

// Nạp vào file thông số hình khối tạo bởi Physics Editor
GB2ShapeCache::sharedGB2ShapeCache()->addShapesWithFile("shapedefs.plist");

// Bắt sự kiện Touch

auto touchListener = EventListenerTouchOneByOne::create();
touchListener->setSwallowTouches(true);
touchListener->onTouchBegan= CC_CALLBACK_2(HelloWorld::onTouchBegan,this);
touchListener->onTouchMoved=CC_CALLBACK_2(HelloWorld::onTouchMoved,this);
touchListener->onTouchEnded=CC_CALLBACK_2(HelloWorld::onTouchEnded,this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener,this); 

// Cập nhật Scene
schedule( schedule_selector(HelloWorld::update) );

    return true;
}

3 Hàm Touch

void HelloWorld::onTouchEnded(Touch* touch, Event* event)
{
// Lấy tọa độ điểm touch và tạo ra đối tượng tại tọa độ đó
auto touchPoint = touch->getLocation(); 
touchPoint = this->convertToNodeSpace(touchPoint);
addNewSpriteWithCoords( touchPoint );
}

bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
{
return true;
}

void HelloWorld::onTouchMoved(Touch* touch, Event* event)
{
}

Hàm addNewSpriteWithCoords

// Chuỗi string lưu tên các file ảnh nằm trong pack
string names[] = {
"hotdog",
"drink",
"icecream",
"icecream2",
"icecream3",
"hamburger",
"orange"
};

void HelloWorld::addNewSpriteWithCoords(Point p)
{
string name = names[rand()%7]; // Random 0-6

// Trả về xâu ký tự ví dụ: "orange.php" - = phương thức c_str()
Sprite *sprite = Sprite::createWithSpriteFrameName((name+".png").c_str());

sprite->setPosition(p);
addChild(sprite);

// Tạo b2BodyDef
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);
bodyDef.userData = sprite;

b2Body *body = world->CreateBody(&bodyDef);

// Tạo fixture cho body, bằng cách sử dụng GB2ShapeCache

GB2ShapeCache *sc = GB2ShapeCache::sharedGB2ShapeCache();
sc->addFixturesToBody(body, name.c_str()); 
sprite->setAnchorPoint(sc->anchorPointForShape(name.c_str())); // Đặt cùng 1 điển neo
}

Hàm update, chỉ việc copy lại ở những bài trước thôi, nếu có thay đổi thì đổi giá trị của velocityIterations + positionIterations 


void HelloWorld::update(float dt)

{

int velocityIterations = 8;
int positionIterations = 1;
world->Step(dt, velocityIterations, positionIterations);

for (b2Body *body = world->GetBodyList(); body != NULL; body = body->GetNext())   
if (body->GetUserData()) 
{  
Sprite *sprite = (Sprite *) body->GetUserData();  
sprite->setPosition(Point(body->GetPosition().x * PTM_RATIO,body->GetPosition().y * PTM_RATIO));  
sprite->setRotation(-1 * CC_RADIANS_TO_DEGREES(body->GetAngle())); 

}  
world->ClearForces(); 
world->DrawDebugData();   

}

Và đây là hàm Draw để Debug cho Box2D ( V3.0, 3.1 thì như bên dưới ). V3.2 RC0 trở lên thì thay đổi 1 chút ở phần tham số vào virtual void draw(Renderer *renderer, const Mat4 &transform, uint32_t flags). Các bạn chú ý. Nội dung hàm mình ko giải thích nhé, lấy từ thư viện ra thôi.

void HelloWorld::draw(Renderer *renderer, const Mat4 &transform, bool transformUpdated)
{
GL::enableVertexAttribs( GL::VERTEX_ATTRIB_FLAG_POSITION );
Director* director = Director::getInstance();
CCASSERT(nullptr != director, "Director is null when seting matrix stack");
director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
world->DrawDebugData();
director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
}

OK rồi đó, Build thử xem cơm cháo thế nào

Cũng khá hay phải không nào.
Trong bài này chúng ta đã được nghâm cứu các vấn đề sau:

+ Cách sử dụng Physics Editor và TexturePacker ( có thể = ShoeBox hoặc SpritePacker thay thế )
+ Phân tích body của 1 Sprite phức tạp = thư viện GB2ShapeCache
+ Debug drawing trong Box2D


Mình kết thúc bài này ở đây nhé, chào và hẹn các bạn ở bài sau!

Cách build Project cpp-tests của Cocos2d-x V3.x ra EXE (publish not debug)

Hi các bạn!

Dạo này mình tập trung nghiên cứu mấy project game lớn lớn 1 chút để tích kinh nghiệm và đào sâu hơn các thư viện mẫu của Engine nên cũng ít viết bài hơn. Mọi người thông cảm nhé. Càng đào sâu đọc kỹ càng thấy những hiểu biết trước đây có nhiều điểm chưa đúng, hoặc không còn đúng vào thời điểm hiện tại. Như thế là tốt, chứ cứ bảo thủ với kiến thức cũ của mình thì nguy. Chính vì thế mình đang chỉnh sửa lại những bài viết cũ trên Blog.

Trong 1 bài ngắn ngắn này mình sẽ chỉ cho các bạn cách Publish Project cpp-test của Engine ra file chạy Exe trong Window - có thể mở trực tiếp nhiều lần ko cần dùng VS. Không xét việc build APK nhé, bạn làm việc nhiều trên máy tính để code hơn mà.

Các bạn biết đấy, 1 project bình thường tạo bởi Engine, bạn có thể build ra exe bằng 2 cách :

+ Dùng lệnh cmd của window
+ Dùng VS2012, 2013 để tạo bản Release hoặc Debug

Nhưng với cách tạo Release hoặc Debug mình chạy trực tiếp file exe sẽ lỗi do thiếu Resource, và rất nhiều lib,obj, log, linh tinh khác trong thư mục. Mỗi lần chạy lại phải mở VS lên khá bất tiện. Có thể mình chưa biết cách Publish trong VS chăng. Có ai biết không - Cách build 1 lần rồi mở file Exe lên chạy cho các lần khác đó?

Với 1 bản Publish bạn sẽ dễ dàng sử dụng mọi lúc mọi nơi trên window, kể cả vác sang máy khác để chạy thử mà ko phải mở VS lên.

Thông thường khi bạn down Engine về, bạn build Project cpp-test bằng cách sau

+ Mở file cocos2d-win32.vc2012.sln theo đường dẫn Q:\ANDROID\Cocos2dx3\build, rồi cứ thế Debug và chạy thử, lần nào muốn xem ví dụ mẫu bạn cũng sẽ phải làm như vậy khá bất tiện nhỉ. Mình chuyển chế độ Release cũng vẫn phải làm vậy là sao. Nên loay hoay tìm ra cách build cmd cho cpp-tests kia. đơn giản thôi, chẳng cao siêu gì, nhưng có thể có người chưa biết như mình chẳng hạn.

Cách của mình là như thế này

+ Dùng lệnh cmd của window buil Project cpp-test như là 1 project thông thường các bạn tạo ra, nhưng trước hết các bạn phải chỉnh sửa 2 file sau.

1/ Copy file cocos2d-win32.vc2012.sln ở trên vào Project cpp-test theo đường dẫn Q:\ANDROID\Cocos2dx3\tests\cpp-tests\proj.win32, đổi tên lại thành cpp-tests.sln

2/ Quan trọng đây : Sửa path của các Libs trong file đó theo đúng đường dẫn mới ( do mình copy và thả vào thư mục khác với thư mục build mà ). Có chút kinh nghiệm là sửa được thôi. Mà DOWNLOAD file của mình cho nhanh . Trong này mình đã bỏ đi Lib và Project test khác nhé chỉ để cpp-tests thôi

3/ Mở file .cocos-project.json Xóa hết ( tốt hơn là lưu Save As đề phòng ) config trong đó và paste

{
    "project_type": "cpp"
}

Save lại

OK, giờ thì BUILD thử bằng commend sau

>cocos run -s (chỗ này bạn kéo thả thư mục cpp-tests vào đây nhé, vì path dài ngại gõ) -p win32

Kết quả sẽ ra thư mục Publish nằm trong thư mục Cpp-tests với đầy đủ Resource, dll, EXE.
Khi nào bạn cần chạy thử các ví dụ chỉ việc vào đây, hoặc tạo shortcut ngoài desktop mà run thôi. đỡ phải dùng đến VS nữa, và cũng ko phải tạo APK chạy trên điện thoại.

Bạn hoàn toàn có thể mang thư mục Publish này lên máy window khác để chạy nhé



Có thể có cách khác nữa nhưng mình chỉ biết các này thôi, ai biết cách ngắn gọn hơn thì share với

+ Bạn cũng có thể build lua-tests với cách tương tự nhưng nhớ chú ý file json là .lua nhé

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

Saturday, July 5, 2014

Bài 27: Học làm game thứ 4 - Game đập chuột ( Part 2)

Hi các bạn!

Mấy ngày xem bóng hoa mắt quá đi, cũng làm tí cá độ cho vui, rất may là chưa có cái gì ra đi cả hehe. Nay trở lại hoàn thành nốt phần 2 game đập chuột này nhé ( Máy tính vẫn còn mà ).

Nhiệm vụ chính trong phần 2 này bao gồm các công việc như sau:
+ Đập chuột.
+ Tính điểm + Game Over
+ Add âm thanh.
+ Làm một chút hiệu ứng animation.

Đi luôn nhé!

Bạn mở file HelloWorldScene.h ra, thêm vào đoạn code sau vào phần public

// Tạo 1 Animation với các thông số, Tên ảnh, thứ tự hình ảnh, số khung, trễ
Animation* createAnimation(string prefixName, int* pFramesOrder, int framesNum, float delay);
bool onTouchBegan(Touch *touch, Event *unused_event); // Sự kiện Touch
// Mở , khóa chế độ đập chuột
void unsetTappable(Object* pSender);
void setTappable(Object* pSender);

// Cười + Đánh chuột
Animation *laughAnim;
Animation *hitAnim;
// Label dùng để hiển thị điểm
LabelTTF *label;

// Điểm số
int score;
// Số chuột không bị đập để tính game over
int totalSpawns;
bool gameOver;

nhớ thêm using namespace std; ở đầu file nhé ( vì dùng string )

Tiếp theo mở file HelloWorldScene.cpp ra

Include thêm #include "SimpleAudioEngine.h", và using namespace std;

Trong hàm init() thêm vào đoạn code sau ( trước return true )

auto visibleSize = Director::getInstance()->getWinSize();
auto visibleOrigin = Director::getInstance()->getVisibleOrigin();

//Tạo Animation chuột cười + Hit lên đầu chuột
int laughAnimOrder[6] = { 1, 2, 3, 2, 3, 1 }; // thứ tự các hình ảnh duyệt trong spritesheet
laughAnim = this->createAnimation("mole_laugh", laughAnimOrder, 6, 0.1f);
int hitAnimOrder[6] = { 1, 2, 3, 4 }; // Tương tụ trên
hitAnim = this->createAnimation("mole_thump", hitAnimOrder, 4, 0.02f);
// Nạp vào bộ đệm animation và đặt tên
AnimationCache::getInstance()->addAnimation(laughAnim, "laughAnim");
AnimationCache::getInstance()->addAnimation(hitAnim, "hitAnim");

//Touch
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);

//Tạo 1 Label hiển thị điểm số
float margin = 10;
label = LabelTTF::create("Score: 0", "fonts/Marker Felt.ttf", 14);
label->setAnchorPoint(Point(1, 0));
label->setPosition(visibleOrigin.x + visibleSize.width - margin, 
visibleOrigin.y + margin);
this->addChild(label, 10);

//Khởi tạo các giá trị : điểm, tổng số chuột, game over
gameOver = false;
totalSpawns = 0;
score = 0;

// Nạp trước Âm thanh, play nhạc nền
CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect("laugh.mp3");
CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect("ow.mp3");
CocosDenshion::SimpleAudioEngine::getInstance()->playBackgroundMusic("whack.mp3", true);

Hàm Touch

bool HelloWorld::onTouchBegan(Touch *touch, Event *unused_event)
{
// Lấy tọa độ điểm Touch
Point touchLocation = this->convertTouchToNodeSpace(touch);
// Duyệt vector molesVector
for (Sprite *mole : molesVector) 
{
if (0 == mole->getTag()) continue; // bỏ qua, do đã ko được phép đập
// Lấy đường bao của Sprite ( dạng Rectang ), nếu chứa điểm Touch - tức là đập trúng chuột thì
if ( mole->getBoundingBox().containsPoint(touchLocation) ) 
{
// Âm thanh
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("ow.mp3");
// Cộng điểm và gán tag - ko đập thêm được
mole->setTag(0);
score += 10;

mole->stopAllActions(); // Dừng tất cả Action
// Tạo hitAnimation từ Cache
Animate *hit = Animate::create(hitAnim);
// Thụt xuống + biến mất
MoveBy *moveDown = MoveBy::create(0.2f, Point(0, -mole->getContentSize().height));
EaseInOut *easeMoveDown = EaseInOut::create(moveDown, 3.0f);
// Thực hiện 2 Animation tuần tự, bị hit và thụt xuống
mole->runAction(Sequence::create(hit, easeMoveDown, NULL));
}
}

return true;
}


Hàm createAnimation

Animation* HelloWorld::createAnimation(string prefixName, 
  int* pFramesOrder, 
  int framesNum, 
  float delay)
{
// Hãy xem lại bài 19 về cách làm animation từ spritesheet, có 5 bước nhỏ như sau
Vector<SpriteFrame*> animFrames; //1
// Tạo frame
for (int i = 0; i < framesNum; i++) //2
{
char buffer[20] = { 0 }; //3
sprintf(buffer, "%d.png", pFramesOrder[i]); //3
string str = prefixName + buffer; //3 Chính là tên của ảnh trong spritesheet 
// tạo frame, add vào vector
auto frame = SpriteFrameCache::getInstance()->getSpriteFrameByName(str); //4
animFrames.pushBack(frame); //4
}
// Trả về 1 animation
return Animation::createWithSpriteFrames(animFrames, delay); //5
}


Hàm tryPopMoles, sửa lại 1 chút, hàm này là hàm update Scene theo thời gian, mình sẽ làm các việc kiểm tra gameOver, tính điểm trong này, vì các sự kiện này luôn phụ thuộc thời gian mà.

void HelloWorld::tryPopMoles(float dt)
{

if (gameOver) return; // Game over rồi thì thôi

// Hiển thị điểm = Label, tất nhiên có cách hiển thị điểm = font, text, mình ko xét ở đây
char scoreStr[30] = { 0 };
sprintf(scoreStr, "Score: %d", score);
label->setString(scoreStr);

// Xuất hiện đủ 50 chú chuột thì kết thúc game
if (totalSpawns >= 50) {
Size winSize = Director::getInstance()->getWinSize();
// Kết thúc game
LabelTTF *goLabel = LabelTTF::create("Game Over!", "fonts/Marker Felt.ttf", 48.0f);
goLabel->setPosition(Point(winSize.width / 2, winSize.height / 2));
goLabel->setScale(0.1f);
this->addChild(goLabel, 10);
goLabel->runAction(ScaleTo::create(0.5f, 1.0f)); // Tạo hoạt cảnh 1 tí cho đẹp

gameOver = true; // gán game Over = true để timeline sau ko hiện ra label kết thúc game nữa, bạn test = false xem sẽ thấy
return;
}
//-----------
// Code vòng for ở bài trước vẫn thế nhé vẫn thế nhé

}

2 Hàm khóa và mở khóa trạng thái " được đập" và "ko được đập "

void HelloWorld::setTappable(Object* pSender) // được phép đập
{
Sprite *mole = (Sprite *)pSender;
mole->setTag(1); // cho phép đập
//Play âm thanh, --- Cứ cười đi rồi ăn đập nhá ku!
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("laugh.mp3");
}

void HelloWorld::unsetTappable(Object* pSender)
{
Sprite *mole = (Sprite *)pSender;
mole->setTag(0); // Ko cho phép đập
}

Giờ còn mỗi hàm PopMole này nữa là xong

void HelloWorld::popMole(Sprite *mole)
{
if (totalSpawns > 50) return; // Kết thúc khi đủ 50
totalSpawns++; // đếm số chuột thò lên

// Lệnh này ko rõ lắm, vì bỏ đi game vẫn chạy mole->setSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("mole_1.png"));

// Action của chuột, nhô lên, thụt xuống
auto moveUp = MoveBy::create(0.2f, Point(0, mole->getContentSize().height));  // 1
auto easeMoveUp = EaseInOut::create(moveUp, 3.0f); // 2
auto easeMoveDown = easeMoveUp->reverse(); // 3

// Aimaition Cười
Animate *laugh = Animate::create(laughAnim);

// Thực hiện tuần tự các Action: Thò lên, MỞ BỤP, Cười, KHÓA ĐẬP, Thụt xuống
mole->runAction(Sequence::create(easeMoveUp, 
CallFuncN::create(CC_CALLBACK_1(HelloWorld::setTappable, this)),
laugh,
CallFuncN::create(CC_CALLBACK_1(HelloWorld::unsetTappable, this)),
easeMoveDown, 
NULL)); // 5
}

Xong hết code rồi, BUILD & BỤP thôi anh em




  
OK. Vậy là chúng ta đã ngâm cứu xong 1 game nhẹ nhàng nữa rồi. Cũng không quá khó nhỉ, các bạn nhớ đọc lại code vài lần nhé để nhớ cho lâu.

Vì là bản game TUT nên vẫn còn một số hạn chế:

+ Không có búa đập cho đã tay
+ Đập vào đầu chuột hiệu ứng chưa đẹp, và rõ ràng lắm
+ Điểm số label, không đẹp bằng image
+ Hơi ít lỗ chuột, làm nhiều 1 chút là đập mỏi tay, hỏng màn hình như chơi
+ Thêm vài màn chơi nữa là đẹp
+ Thêm được phần Pause game cũng ko tồi

- Đó là một số phần mở rộng của game, các bạn có thể xây dựng thêm, và mở rộng tới đâu là tùy thuộc ý tưởng của mỗi người nữa. Mình chỉ tới đây thôi.

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

Thursday, July 3, 2014

Bài 26: Học làm game thứ 4 - Game đập chuột ( Part 1)

Hi cả nhà!

Lâu lâu lại lười viết bài, vì thật ra cũng càng ngày càng khó, và project cũng dài nữa nên.... Và mình cũng đang bí ở phần áp dụng Box2D body cho Animation SpriteSheet, và Box2D body cho CocoStudio, Spine. Tìm hiểu mãi mà chưa có cách làm. Bạn nào đã từng code với bản 2.x có đoạn code áp cho 1 trong 3 cái trên thì chia sẻ giúp nhé, cám ơn.

Hôm nay mình tìm được 1 game khá thú vị, đưa lên đây cho mọi người cùng tìm hiểu, code khá đơn giản, chủ yếu là ôn tập lại các kiến thức đã học thôi, không phức tạp lắm đâu. Đó là game Đập chuột ( Whack Hole ).

Trong này có 1 -3 chú chuột chũi nhô lên, thụt xuống ở 3 cái lỗ, nhiệm vụ của bạn là đập vào đầu nó để ghi điểm thôi. Trong Part 1 ngày, chúng ta nghiên cứu những vấn đề sau

+ Thay đổi tỉ lệ màn hình với từng loại thiết bị
+ Dựng Scene
+ Action của Chuột

Thế thôi nhỉ, ta bắt đầu!

B1 - Thay đổi tỉ lệ màn hình theo từng loại thiết bị

Bạn tạo mới Project dapchuot, mở file AppDelegate.cpp ra, thêm vào đoạn code sau, bên dưới lệnh director->setAnimationInterval(1.0 / 60);

    // Màn hình thiết bị
    Size frameSize = glview->getFrameSize();  
    auto designSize = Size(512, 384);  // Khung hình thiết kế

    // Độ phân giải thiết kế
    glview->setDesignResolutionSize(designSize.width, designSize.height, ResolutionPolicy::NO_BORDER);

    // Vector <string> lưu thông tin đường dẫn Resource
    std::vector<std::string> searchPaths;
    if (frameSize.height > 480)  // Nếu màn hình > 480
    {
        searchPaths.push_back("hd"); // đường dẫn là ("hd");
        Size resourceSize = Size(1024, 768); // Đặt kích thước = 1024,768


        // Tăng kích thước content để phù hợp với màn hình
        director->setContentScaleFactor(MIN(resourceSize.height/designSize.height,
                                            resourceSize.width/designSize.width));
    }
    else //  Chọn sd< 480px
    {
        searchPaths.push_back("sd");
    }

    // Chọn Resource
    FileUtils::getInstance()->setSearchPaths(searchPaths);

B2 - Dựng màn chơi 

Bạn mở file HelloWorldScene.h, thêm vào đoạn code sau

void tryPopMoles(float dt);  // 1 hàm rất giống hàm update ( float dt ) đúng ko, thì chức năng cũng giống thế mà, update scene theo thời gian
void popMole(Sprite *mole); // Hàm acction cho chuột

Vector<Sprite*> molesVector; // Vector Sprite lưu các chú chuột

Mở HelloWorldScene.cpp

Trong hàm init (), thêm 1 đoạn code khá dài sau ( xóa phần code thừa bên trong, chỉ chừa lại phần super init() và return true nhé)

    // Đặt tên cho các file Resource, lưu ý 1 chút với string nó thuộc lớp std nên phải khai báo dạng std::string. Nếu không muốn thế này bạn phải dùng namespace: using namespce std; ở đầu file
    
    // Các dạng file này .pvr.ccz, .plist nhìn có vẻ ghê ghớm nhưng thực ra không có gì cả. File .plist chứa thông tin tọa độ, tên gọi, kích thước của các ảnh png đơn lẻ được pack trong file .pvr.ccz. Còn file .pvr.ccz là 1 dạng nén ảnh để giảm kích thước lưu trữ và bộ nhớ trong game. Bạn dùng phần mềm TexturePacker 3.3.x trong Blog này mình có bài rồi để thực hiện pack nhé

    std::string bgSheet = "background.pvr.ccz";
    std::string bgPlist = "background.plist";
    std::string fgSheet = "foreground.pvr.ccz";
    std::string fgPlist = "foreground.plist";
    std::string sSheet  = "sprites.pvr.ccz";
    std::string sPlist  = "sprites.plist";
    
    // Nạp background + foreground
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile(bgPlist);
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile(fgPlist);
    
    // Add background, bg_dirt.png được lấy từ background.pvr.ccz (  dựa trên thông số của background.plist)
    Sprite *dirt = Sprite::createWithSpriteFrame(
                        SpriteFrameCache::getInstance()->getSpriteFrameByName("bg_dirt.png"));
    dirt->setScale(2.0); // Phóng to lên
    dirt->setPosition(winSize.width/2, winSize.height/2);
    this->addChild(dirt, -2);

    // Add foreground, grass_lower.png lấy trong foreground.pvr.ccz ( dựa trên thông số của foreground.plist )
    // Nửa dưới
    Sprite *lower = Sprite::createWithSpriteFrame(
                        SpriteFrameCache::getInstance()->getSpriteFrameByName("grass_lower.png"));

    // Bạn chú ý chỗ này, 1 hình ảnh có nhiều AnchorPoint ( là điểm engine sẽ lấy để đặt trên màn hình vào tọa độ xác định. thường có 9 AnchorPoint hay dùng là các điểm sau: 4 góc hình + 1 tâm + 4 trung điểm 4 cạnh của khung ảnh png
    //Như thế này 
    //Point(0,0) = góc trái-dưới
    //Point(0,1) = góc trên-trái
    //Point(1,0) = góc phải-dưới
    //Point(1,1) = góc trên-phải
    //Point(0.5,0.5) = Tâm
    //Point(0.5,0) = giữa cạnh dưới
    //Point(0,0.5) = giữa cạnh trái
    //Point(0.5,1) = giữa cạnh trên
    //Point(1,0.5) = giữa cạnh phải
    // Với hình đặc biệt trong CocoStudio, Spine, SpriteSheet, ta có thể chỉnh được điểm neo này = tay
    // Tùy vào từng trường hợp mà ta set AnchoPoint hợp lý sẽ khiến việc căn đối tượng dễ dàng hơn

    lower->setAnchorPoint(Point(0.5, 1));
    lower->setPosition(winSize.width/2, winSize.height/2 + 1); // Tách 2 nửa ra 1 chút để nhận biết, trong game bạn xóa + 1 đi
    this->addChild(lower, 1);

    // Nửa trên, tại sao có nửa dưới và trên ? khi bạn build xong sẽ hiểu, vì nửa trên set index -1, nửa dưới =1 để cho chuột có index ở giữa = 0 sẽ hiện bên trên nửa trên và bị che một phần do nửa dưới. giá trị index có tác dụng sắp sếp đối tượng như thế.

    Sprite *upper = Sprite::createWithSpriteFrame(
                        SpriteFrameCache::getInstance()->getSpriteFrameByName("grass_upper.png"));
    upper->setAnchorPoint(Point(0.5, 0));
    upper->setPosition(winSize.width/2, winSize.height/2 - 1); // Tách 2 nửa ra 1 chút để nhận biết, trong game bạn xóa - 1 đi
    this->addChild(upper, -1);
    
// Tạo SpriteSheet... lưu chuột
SpriteBatchNode *spriteNode = SpriteBatchNode::create(sSheet);
this->addChild(spriteNode, 0);
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(sPlist);

// Nạp 3 chuột
Sprite *mole1 = Sprite::createWithSpriteFrameName("mole_1.png");
mole1->setPosition(99, winSize.height / 2 - 75);
spriteNode->addChild(mole1); // Thêm vào spritesheet
molesVector.pushBack(mole1); // Thêm vào Vector

Sprite *mole2 = Sprite::createWithSpriteFrameName("mole_1.png");
mole2->setPosition(winSize.width / 2, winSize.height / 2 - 75);
spriteNode->addChild(mole2);
molesVector.pushBack(mole2);

Sprite *mole3 = Sprite::createWithSpriteFrameName("mole_1.png");
mole3->setPosition(winSize.width - 102, winSize.height / 2 - 75);
spriteNode->addChild(mole3);
molesVector.pushBack(mole3);

// Update scene trong thời gian 0.5 giây
this->schedule(schedule_selector(HelloWorld::tryPopMoles), 0.5);

Hàm tryPopMoles như sau

void HelloWorld::tryPopMoles(float dt) // hàm này nhận đối số thời gian giống hàm update (float dt)
{
for (Sprite *mole : molesVector) // duyệt vector
{
int temp = CCRANDOM_0_1()*10000; // Tạo số random
if ( temp % 3 == 0)  // chia hết cho 3, thì ....
{
if (mole->getNumberOfRunningActions() == 0)  // Nếu ko có Action nào
{
this->popMole(mole); // Thò đầu lên và ăn đập : ))
}
}
}
}

B3 - Hành động của chuột

void HelloWorld::popMole(Sprite *mole)
{
// Lên 
auto moveUp = MoveBy::create(0.2f, Point(0, mole->getContentSize().height));  // 1
auto easeMoveUp = EaseInOut::create(moveUp, 3.0f); // 2

auto easeMoveDown = easeMoveUp->reverse(); // 3
auto delay = DelayTime::create(0.5f); // 4

// Thực hiện chuỗi hành động, Lên, đứng đó, Xuống, ko làm gì cả 
mole->runAction(Sequence::create(easeMoveUp, delay, easeMoveDown, NULL)); // 5
}

Xong rồi, Build chạy thử xem có ra gì không, ngon rồi!




Vậy là trong bài này chúng ta cùng nhau ôn lại 1 vài kiến thức sau:

+ SpriteSheet
+ Nạp Resource từ file Pvr, plist
+ Vector
+ Action

Đơn giản thế thôi, bài sau chúng ta sẽ thực hiện nốt công việc là, đập chuột, tính điểm.

Dowload Resoucrce, Class

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

Bài 27: Học làm game thứ 4 - Game đập chuột ( Part 2)