Hiển thị các bài đăng có nhãn My Game. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn My Game. Hiển thị tất cả bài đăng

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

Người đăng: share-nhungdieuhay on Thứ Tư, 6 tháng 8, 2014


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ờ?

More about

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

Người đăng: share-nhungdieuhay on Thứ Sáu, 1 tháng 8, 2014

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 )

More about

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

Người đăng: share-nhungdieuhay on Thứ Bảy, 5 tháng 7, 2014

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.

More about

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

Người đăng: share-nhungdieuhay on Thứ Năm, 3 tháng 7, 2014

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) 
More about