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ơ.

Monday, May 30, 2016

Body ( Box2D ) Cho Animation SpriteSheet



Hi!

Lâu lâu không vào thăm Blog, vẫn thấy có lượt xem bài viết. Tiếc là không có thời gian để mà viết tiếp thôi.



Trong bài này mình viết nhanh. Nội dung là tạo body cho Animation, loại body bám sát với đối tượng, chứ ko phải body hình vuông, hình tròn nhé




Các bạn mới học Cocos2dx, Physic, sau khi đọc xong các bài viết tạo physicbody cho Sprite đơn giản sẽ luôn có một thắc mắc: Làm sao để tạo Body cho 1 Animation ( dạng SPrite Sheet ). Trước đây mình cũng đã từng vướng vào vấn đề này mà không thể tìm thấy tài liệu ở bất kì đâu. Về sau mình mới biết được rằng ko phải là không có ai làm, hoặc không làm được mà là vì, Cái này có nhiều hạn chế, cuối bài mình sẽ nói

Nhưng giờ đây, mình sẽ trình bày ý tưởng như này

1 Animation có đặc thù là: sau mỗi khoảng thời gian DelayUnit sẽ load 1 ảnh từ Resource để hiển thị lên màn hình. do DelayUnit nhỏ nên ta tưởng như nhân vật đang chuyển động, nhưng thực chất đều là ảnh tĩnh hết.

Vậy làm thế nào để tạo Body => đơn giản thôi, mình sẽ tạo Body đồng bộ với SpriteFrame tại thời điểm DelayUnit thế là xong.

Việc đồng bộ này diễn ra ở hàm Update() nhé. Dùng 1 biến điểm thời gian, khi tới thời điểm DelayUnit thì sẽ load Body vào, đồng thời xóa body của Frame trước đi.

Bạn đã biết cách tạo body chính xác đối tượng. Hãy tìm lại 1 bài trong Blog nhé
Bạn đã biết tạo Animation . Hãy tìm lại trong Blog.

Bắt đầu thôi:

Lúc đầu mình sử dụng Hàm sprite->runAction(Animation); => Vẫn tạo được Body, vẫn khớp nhưng nó bị một hạn chế là KHÔNG ĐỒNG BỘ về THỜI GIAN. Khi kéo khung Window sẽ thể hiện rõ điều này. dần dần Body và Ani sẽ lệch pha nhau.

Do đó, mình sẽ dùng không thèm Action có sẵn nữa mà sẽ chơi kiểu Nhà quê: Tự tạo Animation bằng tay. Nghe có vẻ ghê ghớm, chứ thực ra là thế này.

Tại mỗi 1 DelayUnit mình sẽ load 1 ảnh thay thế cho ảnh trước đó. Vậy là xong Animation

Đồng thời để tạo Body, thì cũng tại DelayUnit bạn nạp Body ( từ file .plist ) file này tạo bằng soft PhysicEditor (Hãy tìm lại 1 bài trong Blog nhé)

Xong phim. Cách này xử lý được vấn đề ĐỒNG BỘ Thời gian


Hàm này tạo Animation theo DELAY Time, quẳng trong update() là sẽ có animation
void HelloWorld::updateAnimation(float dt){

    timer += dt;
    if (timer>=DELAY)
    {

        bear->setDisplayFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName(String::createWithFormat("bear%d.png", bear_index)->getCString()));

        //bear->setFlipX(true);

        world->DestroyBody(bearbody);

        createBearB2body(bear_index);

        if (bear_index == 8)
        {
            bear_index = 1;

        }

        timer = 0.0f;
        bear_index++;
    }

}


Hàm này tạo body = cách load file .plist tạo sẵn

void HelloWorld::createBearB2body(int index)
{


    b2BodyDef bodyDef;
    bodyDef.type = b2_staticBody;

    bodyDef.position.Set(bear->getPositionX() / PTM_RATIO, bear->getPositionY() / PTM_RATIO);
    bodyDef.userData = bear;

    bearbody = world->CreateBody(&bodyDef);


    GB2ShapeCache *sc = GB2ShapeCache::sharedGB2ShapeCache();
    sc->addFixturesToBody(bearbody, String::createWithFormat("bear_0%d", index)->getCString());
    bear->setAnchorPoint(sc->anchorPointForShape(String::createWithFormat("bear_0%d", index)->getCString()));

}



Mình chỉ ngắn gọn vậy thôi, và không viết code ra đây nhé, các bạn down CODE về test thôi

FULL CODE đây. Mai up nhé. Trong code mình cũng giải thích, chú thích gì đâu nha

Giờ là tới phần nhược điểm

Về cách tạo body Animation:

Bạn nào học kỹ về Physic + phân tích đặc điểm của ANimation ( đặc biệt là cái DelayUnit ) sẽ có cách làm ngay

Tuy nhiên: có 1 vài nhược điểm

FLIP ANIMATION: OK, flip được, nhưng  bạn ko flip được BODY đâu, mình ko phải chuyên gia về BOX2D nhưng hình như ko có flip Body.

Move ANimation: mình chưa thử nhé, có thể được, vì chỉ cần đặt vị trí body theo Sprite

Rotation: chưa thử, có thể lỗi

Cách tạo khá Amature

Và việc xóa body, tạo body liên thục theo dt không phải là Tốt cho game

Thế nhé mai mình up code tham khảo

Để tạo Body + animation phức tạp và PRO hơn, hãy dùng Spine ( 2D ). 3D thì mình chưa có điều kiện...




Monday, May 25, 2015

Cài đặt Cocos2dx 3.6 trên Window + Mac OS X

Hi, những ai đang theo dõi blog này, Bỏ bê quá lâu rồi, mấy lần định quay lại nhưng thực là không có thời gian.

Trong bài này mình viết ngắn gọn thôi nhé, vì ai đọc bài 3 về cách cài đặt Cocos2dx 3.0 thì chắc là hiểu cách làm cả. Tuy nhiên nay mình đọc lại thấy nó dài dòng quá, tại hồi đó, version 3 mới ra, cài đặt cũng hơi lằng nhằng, đúc kết mãi mới có kinh nghiệm, nhưng giờ thì cài dễ như ăn cháo. Cách làm thì thế này

Thursday, August 7, 2014

Bài 30: Học làm game thứ 5 - Space Ship ( Part 2 - End )


Hi, Rảnh rỗi tranh thủ viết cho xong game Space Ship này.
Bài trước chúng ta đã thiết kế sơ bộ xong phần màn chơi, song còn thiếu một số phần quan trọng trong game nên có mà chúng ta sẽ bổ sung ngay sau đây:

+ Bắn đạn khi Touch màn hình
+ Bắt sự kiện va chạm giữa đạn và thiên thạch
+ Tính điểm
+ Game Over

Đơn giản có thế thôi, chúng ta sẽ lướt nhanh!

B1: Bắn đạn khi Touch màn hình

Bạn mở file HelloWorldScene.h thêm vào dòng lệnh sau, trong public

// Hàm bắt sự kiện touch, dùng multiTouch, hoặc Touch thôi cũng được
void HelloWorld::onTouchesBegan(const std::vector<Touch*>& touches, Event *event)

Tiếp đó trong HelloWorldScene.cpp ta thiết kế hàm này như sau

void HelloWorld::onTouchesBegan(const std::vector<Touch*>& touches, Event *event)
{
SimpleAudioEngine::getInstance()->playEffect("laser_ship.wav"); // Âm thanh

Size winSize = Director::getInstance()->getWinSize();

// Lấy sprite Laser từ bộ lưu trữ Vector
Sprite *shipLaser = (Sprite *) _shipLasers->at(_nextShipLaser++);

if ( _nextShipLaser >=_shipLasers->size())   // Reset index laser
_nextShipLaser = 0;
// Đặt vị trí ở phía mũi tàu, và cho hiện lên
shipLaser->setPosition(Point(_ship->getPosition().x + shipLaser->getContentSize().width/2, _ship->getPosition().y));
shipLaser->setVisible(true);
// set body
auto laserbody = PhysicsBody::createBox(shipLaser->getContentSize()/2);  

laserbody->setContactTestBitmask(0xf);  
laserbody->setDynamic(true);
shipLaser->setPhysicsBody(laserbody);

// Di chuyển đạn, gọi tới hàm setInvisible để xử lý
shipLaser->stopAllActions();
shipLaser->runAction(Sequence::create( 
MoveBy::create(0.5,Point(winSize.width, 0)),
CallFuncN::create(this, callfuncN_selector(HelloWorld::setInvisible)), 
NULL 
));
}

B2: Bắt sự kiện va chạm

Thêm hàm sau vào file HelloWorldScene.h

bool onContactBegin(const PhysicsContact &contact);

Và xây dựng nó trong file HelloWorldScene.cpp như sau

bool HelloWorld::onContactBegin(const PhysicsContact& contact)    
{
auto laser = (Sprite*)contact.getShapeA()->getBody()->getNode();
int Tag1 = -1;
if(laser) 
Tag1 = laser->getTag();
auto asteroid = (Sprite*)contact.getShapeB()->getBody()->getNode();
int Tag2 = -1;
if(asteroid) Tag2 =  asteroid->getTag();

//Va chạm giữa đạn và Thiên Thạch
if((Tag1==KLASER&Tag2==KASTEROID)||(Tag2==KLASER&Tag1==KASTEROID))
{
SimpleAudioEngine::sharedEngine()->playEffect("explosion_large.wav"); 
_world->removeBody(laser->getPhysicsBody());
laser->setVisible(false);
_world->removeBody(asteroid->getPhysicsBody());
asteroid->setVisible(false); 
}
// Va chạm giữa thiên thạch và Ship
if((Tag1==KSHIP&Tag2==KASTEROID)||(Tag2==KSHIP&Tag1==KASTEROID))

{
_lives--;

}

return true; 
}

Và không được quên đoạn code Listener ở init()

auto contactListener = EventListenerPhysicsContact::create();
contactListener->onContactBegin = CC_CALLBACK_1(HelloWorld::onContactBegin, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);

OK, giờ là tới phần Tính điểm và GameOver

B3: Tính điểm và GameOver

Trong hàm update() bạn thêm vào 1 đoạn sau đây

if (_lives <= 0) { // Kiểm tra không còn mạng nào thì game Over
_ship->stopAllActions();
_ship->setVisible(false);
_world->removeBody(_ship->getPhysicsBody());
this->endScene(KENDREASONLOSE);   // Game Over

Hàm endScene xây dựng như sau

void HelloWorld::endScene( EndReason endReason ) {

if (_gameOver) // trạng thái game
return;
_gameOver = true;

Size winSize = Director::getInstance()->getWinSize();

char message[10] = "";
if ( endReason == KENDREASONLOSE)
strcpy(message,"You Lose"); 

// Tạo 2 Label để làm thông báo
LabelBMFont * label ;
label = LabelBMFont::create(message, "Arial.fnt");
label->setScale(0.1);
label->setPosition(Point(winSize.width/2 , winSize.height*0.6));
this->addChild(label);


// Tạo 1 nút reset game là 1 label
LabelBMFont * restartLabel;
strcpy(message,"Restart");
restartLabel = LabelBMFont::create(message, "Arial.fnt");

MenuItemLabel *restartItem =  MenuItemLabel::create(restartLabel,CC_CALLBACK_1(HelloWorld::resetGame,this));

restartItem->setScale(0.1);
restartItem->setPosition( Point(winSize.width/2, winSize.height*0.4));

Menu *menu = Menu::create(restartItem, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);

restartItem->runAction(ScaleTo::create(0.5, 1.0));
label ->runAction(ScaleTo::create(0.5, 1.0));
this->unscheduleUpdate(); // dừng update Scene
}

Và nhớ phải thêm thuộc tính bool _gameOver vào phần public của HelloWorldScene.h, đồng thời trong hàm init() phải khởi tạo nó với giá trị false

Bổ sung hàm endScene() và resetGame() vào trong lớp HelloWorld, và hàm resetGame như sau

void HelloWorld::resetGame(Ref* pSender) {
auto scene = HelloWorld::createScene();
Director::getInstance()->replaceScene(TransitionZoomFlipY::create(0.5, scene));  

Size winSize = Director::getInstance()->getVisibleSize();

}

Giờ ta thêm 1 chút phần tính điểm. Khi bắn mỗi thiên thạch ta được 10 điểm.

Bạn thêm 1 thuộc tính int score, LabelBMFont * _scoreDisplay;vào lớp HelloWorldScene, và khi khởi tạo thêm đoạn code này

_scoreDisplay = LabelBMFont::create("Score: 0", "Arial.fnt", 
visibaleSize.width * 0.3f);
_scoreDisplay->setAnchorPoint(Point(1, 0.5));
_scoreDisplay->setPosition(
Point(visibaleSize.width * 0.8f, visibaleSize.height * 0.94f));
this->addChild(_scoreDisplay);

Trong hàm kiểm tra va chạm chúng ta sẽ tính điểm bằng đoạn code nhỏ như thế này

score+=10;
char szValue[100] = { 0 }; // Lấy ra điểm qua mảng đệm char
sprintf(szValue, "Score: %i", score); // Chuyển sang chuỗi => chuỗi
_scoreDisplay->setString(szValue); // Hiện điểm lên

Bạn có thể làm thế với Live để theo dõi số mạng của Ship

OK, Build thử xem kết quả thế nào nhé, cũng không tệ với 1 game "tự tui".

Và sau đây mình làm thêm 1 bước Bonus nữa là 

Bonus: Điều khiển Ship bằng Accelerometer - gia tốc kế

Trước hết bạn copy 2 file VisibleRect.h, .cpp trong bài cpp-tests vào Class của chúng ta. sau đó trong phần init() thêm đoạn code này vào

#define FIX_POS(_pos, _min, _max) \
if (_pos < _min)        \
_pos = _min;        \
else if (_pos > _max)   \
_pos = _max; 

auto listener = EventListenerAcceleration::create([=](Acceleration* acc, Event* event){
auto shipSize  = _ship->getContentSize();

auto ptNow  = _ship->getPosition();

log("acc: x = %lf, y = %lf", acc->x, acc->y);

ptNow.x += acc->x * 9.81f;
ptNow.y += acc->y * 9.81f;

FIX_POS(ptNow.x, (VisibleRect::left().x+shipSize.width / 2.0), (VisibleRect::right().x - shipSize.width / 2.0));
FIX_POS(ptNow.y, (VisibleRect::bottom().y+shipSize.height / 2.0), (VisibleRect::top().y - shipSize.height / 2.0));
_ship->setPosition(ptNow);
});

auto dispathcher = Director::getInstance()->getEventDispatcher();

dispathcher->addEventListenerWithSceneGraphPriority(listener, this);

Vậy thôi, hãy build lại và thử trên ĐT thật, khi nghiêng xem Ship có di chuyển không nhé, nếu di chuyển là đã thành công

Kết thúc bài này, chúng ta cùng nghiên cứu 1 số vấn đề sau

+ Bắn đạn = Touche, duyệt vector
+ Va chạm
+ Tính điểm, game Over
+ Di chuyển Ship bằng gia tốc kế

Download Code

Mình dừng bài học ở đây nhé

Bài 31: Làm game gì bây giờ?

Friday, August 1, 2014

Bài 29: Học làm game thứ 5 - Space Ship ( Part 1 )

Hi, Lâu nay bận kiếm cơm nuôi cái dạ dày nên ít có thời gian post bài. Cơ mà, nếu mọi người đã đọc được tới đây rồi thì đã có thể độc lập nghiên cứu rồi đấy. Mình thấy nhiều bạn tiến cũng khá xa rồi ( lạc hậu rồi ). Tuy nhiên lúc rảnh rỗi mình vẫn cứ viết bài thôi, vì đây là Blog học tập của mình mà, nếu bạn nào rảnh rỗi thì vào ôn luyện lại cũng không sao, có gì mình không đúng thì chỉ giúp, OK?

Trong bài học làm game thứ 5 này, mình sẽ cùng mọi người làm 1 cái game nho nhỏ, tên là Space Ship, tất nhiên là dạng đơn giản thôi, chứ phức tạp quá thì lại phải trình bày, chia part khá nhọc. Mọi người thông cảm, hi vọng trong tương lai sẽ post được những bài công phu, chuẩn mực hơn.

Bài code này mình nhặt nhạnh trên mạng thôi, ở trang http://www.raywenderlich.com/ thì phải, code trên phiên bản 2.x. Mình đã convert lại sang 3.x theo đúng cách làm ở bài trước, và có sửa đổi thêm thắt một chút để chuẩn hơn. Nói trước là bài này chỉ là một bài mẫu nên code khá thô, không thiết kế thành nhiều lớp phức tạp, chức năng cũng đơn giản, không cầu kỳ. Do đó bạn nào muốn tham khảo 1 bài học làm game chuẩn mực: Thiết kế lớp tốt, nhiều chức năng, màu mè đồ họa, code tối ưu, có khả năng tái sử dụng trong nhiều dự án thì không nên đọc bài này. Bạn có thể tìm được những bài nâng cao của Game này ở trên mạng nhé.

Trong Part 1 này, chúng ta sẽ có thể làm được những công việc sau:

+ Tái sử dụng lại lớp Parallax ở bài trước, bài 25
+ Thiết kế màn chơi cho game

Bắt đầu

B1: Tái sử dụng lớp Parallax

Trước tiên bạn tạo 1 Project mới, SpaceShip
Copy 2 file ParallaxNodeExtras.h, .cpp từ bài 25 vào Class
Mở file ParallaxNodeExtras.cpp lên ta sửa hàm updatePosition 1 chút như sau
Xóa hết đoạn code trong lệnh if(po->getChild() == node)
thay bằng đoạn sau

if(po->getChild() == node)

if (node->getContentSize().width<visibleSize.width)
{
po->setOffset(po->getOffset() + Point(visibleSize.width + node->getContentSize().width,0));

}else {
// Mục đích chỗ này áp dụng cho với những đối tượng có chiều rộng > màn hình sẽ di chuyển đúng po->setOffset(po->getOffset() + Point(node->getContentSize().width*2,0));
}

B2: Thiết kế màn chơi

Mở file HelloWorldScene.h lên, lớp này được thiết kế như sau

#include "ParallaxNodeExtras.h"

USING_NS_CC;

using namespace cocos2d;

class HelloWorld : public cocos2d::Layer
{
private:

SpriteBatchNode * _batchNode; // Batch node để lưu các đối tượng có Action
Sprite * _ship;

    ParallaxNodeExtras *_backgroundNode; // Backround là 1 đối tượng Parallax
    Sprite *_spacedust1; // đám bụi 1
    Sprite *_spacedust2; // đám bụi 2
    Sprite *_planetsunrise; // hành tinh
    Sprite *_galaxy;      // thiên hà
    Sprite *_spacialanomaly;  // chịu
    Sprite *_spacialanomaly2;   // chịu

    // Vector để lưu các thiên thạch, dạng con trỏ
    Vector<Sprite*>* _asteroids;
    // Chỉ số để truy cập
    int _nextAsteroid;
    float _nextAsteroidSpawn;  // Thời gian xuất hiện thiên thạch tiếp theo
    float _nextAsteroidtimer;       // Bộ định thời ( hay bộ đếm thời gian, cứ 1 khoảng thời gian thì làm 1 việc gì đó
    // 1 Vector để lưu đạn Laser của tàu, dạng con trỏ
    Vector<Sprite*>* _shipLasers;
    // index
    int _nextShipLaser;
    
    int _lives; // mạng  

    void update(float dt);

    PhysicsWorld* _world;
    void setPhyWorld(PhysicsWorld* world){ _world = world; };
    
public:
    virtual bool init();

    bool onContactBegin(const PhysicsContact& contact);

    static cocos2d::Scene* createScene();
    
    void menuCloseCallback(Ref* pSender);

    CREATE_FUNC(HelloWorld);    

    //Lấy giá trị random trong 1 khoảng
    float randomValueBetween(float low, float high);
    // Ẩn đi
    void setInvisible(Node * node);
    void onTouchesBegan(const std::vector<Touch*>& touches, Event *event); // Multi Touch
};

#endif // __HELLOWORLD_SCENE_H__

OK, sang bước tiếp theo, mở file HelloWorldScene.cpp, ta thiết kế các function như sau:

À nhớ phần include thêm đoạn code sau

#include "SimpleAudioEngine.h"
using namespace CocosDenshion;

using namespace cocos2d;
using namespace CocosDenshion;
using namespace std;

// Định nghĩa các Tag, cho 3 loại đối tượng
enum 
{
KSHIP,
KLASER,
KASTEROID

};

// Hàm tạo Scene với Physics, đơn giản quá rồi
Scene* HelloWorld::createScene()
{
    Scene *scene = Scene::createWithPhysics();
scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
Vect gravity(0.0f, 0.0f); // Vector gia tốc =0
scene->getPhysicsWorld()->setGravity(gravity);  
    HelloWorld *layer = HelloWorld::create();
layer->setPhyWorld(scene->getPhysicsWorld());
    scene->addChild(layer);

    return scene;
}

Hàm HelloWorld::init()

//Nạp Resource

Size visibaleSize = Director::getInstance()->getVisibleSize();
Size winSize = Director::getInstance()->getWinSize();
_batchNode = SpriteBatchNode::create("Sprites.pvr.ccz"); // File này là file ảnh đã mã hóa, tạo bởi TexturePacker nhé, ko mở được bằng trình xem ảnh thông thường, mở = PVR view của soft TexturePacker, hoặc soft tương tự
this->addChild(_batchNode);
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("Sprites.plist");  

//Dựng các vật thể

_ship = Sprite::createWithSpriteFrameName("SpaceFlier_sm_1.png");
_ship->setPosition(Point(visibaleSize.width * 0.1, winSize.height * 0.5));
_ship->setTag(KSHIP); // đặt tag để phân biệt trong va chạm
_batchNode->addChild(_ship, 1); // Insert vào BatchNode để thực hiện Action

// Tạo body cho Ship, -23 là offset, vì ảnh cắt ko chuẩn nên thừa ra khoảng trống nhiều, mình phải giảm bớt kích thước body cho chuẩn,
auto shipBody = PhysicsBody::createCircle(_ship->getContentSize().width / 2-23); 
// Ko có cái này thì không thể xử lý va chạm được
shipBody->setContactTestBitmask(0xf); 
// Va chạm tĩnh
shipBody->setDynamic(false);
_ship->setPhysicsBody(shipBody); 
//Tạo parallax node
_backgroundNode = ParallaxNodeExtras::create();
this->addChild(_backgroundNode,-1) ;

// 2 mảng bụi, add vào Parallax
unsigned int dustQuantity = 2;
for(unsigned int i = 0; i < dustQuantity; i++)
{
auto dust = Sprite::create("bg_front_spacedust.png");
dust->setAnchorPoint(Point(0,0.5));
_backgroundNode->addChild(dust,
0, //order (thứ tự) lớp. Order lớn hơn thì nằm trên che khuất lớp có order nhỏ hơn
Point(0.5, 1), // tốc độ
Point( i*(dust->getContentSize().width),winSize.height/2)); // vị trí
}

// Tạo các vật thể khác, add vào Parallax

_planetsunrise = Sprite::create("bg_planetsunrise.png");
_galaxy = Sprite::create("bg_galaxy.png"); _galaxy->setAnchorPoint(Point(0,0.5));
_spacialanomaly = Sprite::create("bg_spacialanomaly.png");
_spacialanomaly2 = Sprite::create("bg_spacialanomaly2.png");
// Tốc độ di chuyển của vật thể
Point dustSpeed = Point(0.5, 0);
Point bgSpeed = Point(0.05, 0);

// PARALLAX Scolling
_backgroundNode->addChild(_galaxy,-1, bgSpeed, Point(0,winSize.height * 0.7));
_backgroundNode->addChild(_planetsunrise, -1 , bgSpeed, Point(600, winSize.height * 0));
_backgroundNode->addChild(_spacialanomaly, -1, bgSpeed, Point(900, winSize.height * 0.3));
_backgroundNode->addChild(_spacialanomaly2, -1, bgSpeed, Point(1500, winSize.height * 0.9));

// Update scene
this->scheduleUpdate();

// Làm 3 cái Particle trang trí cho game lung linh 1 tí
HelloWorld::addChild(ParticleSystemQuad::create("Stars1.plist"));
HelloWorld::addChild(ParticleSystemQuad::create("Stars2.plist"));
HelloWorld::addChild(ParticleSystemQuad::create("Stars3.plist"));
    
// Giờ thì tạo bộ lưu trữ 

    // Bộ lưu mảng thiên thạch
    #define KNUMASTEROIDS 15
    _asteroids = new Vector<Sprite*>(KNUMASTEROIDS);
    for(int i = 0; i < KNUMASTEROIDS; ++i) {
        Sprite *asteroid = Sprite::createWithSpriteFrameName("asteroid.png");
        asteroid->setVisible(false); // ẩn Sprite vừa tạo, nếu không ẩn, bạn sẽ thấy nó dồn hết về tọa độ 0,0
        asteroid->setTag(KASTEROID);  // đặt tag
        _batchNode->addChild(asteroid); // insert vào batch node
        _asteroids->pushBack(asteroid); // insert vào Vector
    }
    
// Bộ lưu mảng đạn Laser    #define KNUMLASERS 5
    _shipLasers = new Vector<Sprite*>(KNUMLASERS);
    for(int i = 0; i < KNUMLASERS; ++i) {
        Sprite *shipLaser = Sprite::createWithSpriteFrameName("laserbeam_blue.png");
        shipLaser->setVisible(false); 
        shipLaser->setTag(KLASER);
        _batchNode->addChild(shipLaser);
        _shipLasers->pushBack(shipLaser);
    }

 this->setTouchEnabled(true);

    
_lives = 3;
_nextShipLaser =0; // index
_nextAsteroid = 0; // index
_nextAsteroidtimer =0;
_nextAsteroidSpawn = 1.6f; // Cứ 1.6 giây thì xuất hiện 1 thiên thạch
    
// Nạp Audio
    SimpleAudioEngine::getInstance()->playBackgroundMusic("SpaceGame.wav",true);
    SimpleAudioEngine::getInstance()->preloadEffect("explosion_large.wav");
    SimpleAudioEngine::getInstance()->preloadEffect("laser_ship.wav");
return true;

Xong phần init() rùi, giờ tiếp tục dựng một số function game nào

Hàm update(float delta)

Point scrollDecrement = Point(5, 0); // Tốc độ di chuyển của Parallax
Size winSize = Director::getInstance()->getWinSize();

// Update vị trí của Parallax
_backgroundNode->setPosition(_backgroundNode->getPosition() - scrollDecrement);
_backgroundNode->updatePosition();

_nextAsteroidtimer+=delta; // Đếm thời gian

if (_nextAsteroidtimer > _nextAsteroidSpawn) { 

_nextAsteroidtimer = 0; // reset bộ đếm timer

// Vị trí random
float randY = randomValueBetween(0.0,winSize.height);
float randDuration = randomValueBetween(2.0,10.0); // Thời gian di chuyển trên màn hình

// Lấy ra sprite tại index _nextAsteroid
Sprite *asteroid = (Sprite *)_asteroids->at(_nextAsteroid);
_nextAsteroid++;

//reset index
if (_nextAsteroid >= _asteroids->size())
_nextAsteroid = 0;

asteroid->stopAllActions(); // dừng mọi Action
// Đặt lên màn hình, và hiển thị
asteroid->setPosition( Point(winSize.width+asteroid->getContentSize().width/2, randY));
asteroid->setVisible(true);

// Đặt body, -15 là hiệu chỉnh kích thước fix bug của ảnh
auto asbody = PhysicsBody::createCircle(asteroid->getContentSize().width/2-15);
// Xử lý va chạm
asbody->setContactTestBitmask(0xf); 
asteroid->setPhysicsBody(asbody);

// Di chuyển với tốc độ và vị trí cho trước, khi di chuyển tới cuối màn hình, gọi hàm setInvisible, để thực hiện 1 số thao tác
asteroid->runAction(Sequence::create(
MoveBy::create(randDuration, Point( - winSize.width - asteroid->getContentSize().width, 0)), 
CallFuncN::create(CC_CALLBACK_1(HelloWorld::setInvisible,this)), 
NULL
));        
}

OK, giờ xây dựng thêm 1 số hàm phụ là có thể chạy game được rồi

void HelloWorld::setInvisible(Node * node) {
node->setVisible(false); // ẩn đi
_world->removeBody(node->getPhysicsBody()); // remove Body
}

// Lấy Random trong khoảng cho trước
float HelloWorld::randomValueBetween(float low, float high) {
return (((float) rand() / RAND_MAX) * (high - low)) + low;
}


OK rồi đó, build thử game xem có chạy nổi không, Ngon lành nhé

 Cũng khá đẹp đấy chứ!

Trong bài này chúng ta đã cùng nghiên cứu 1 số vấn đề nhỏ sau đây:

+ Sử dụng lại lớp đã được thiết kế từ bài trước, có mở rộng 1 chút
+ Sử dụng Vector để lưu trữ dữ liệu, cái này mình thấy dùng khá nhiều, và cũng khá là hay
+ Tạo bộ lưu trữ đối tượng bằng vector, nạp đối tượng vào bộ lưu trữ khi bắt đầu vào game, ẩn chúng đi...
+ Định thời cho 1 sự kiện theo thời gian

Vậy thôi nhỉ, tuy chỉ là những vấn đề nhỏ, nhưng nhiều vấn đề nhỏ kết hợp lại với nhau nhuần nhuyễn sẽ cho ra vấn đề LỚN đấy.

Download CODE+RESOURCE

Mình dừng bài này ở đây. Hẹn gặp lại

Bài 30: Học làm game thứ 5 - Space Ship ( Part 2 - End )

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!