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 )

23 comments:

  1. Cảm ơn cái nha... Bạn nhiệt tình quá
    Tui cũng kiếm cơm nửa, nuôi tới 2 cái dạ dày lựng...hihi

    ReplyDelete
    Replies
    1. ban oi dowload code roi sao bo zo no khong chay'

      Delete
  2. Ko liên quan đến bài 1 chút nhưng bác có biết cái Follow ko ạ.

    ReplyDelete
    Replies
    1. Không bạn à, nó là gì thế?

      Delete
    2. Cho nó theo dõi 1 thằng nào đó ấy bác

      Delete
  3. bác hay dùng cocos2d::vector hay std::vector thế.
    Mình thì hay dùng của std, thấy dùng nó thì ko cần retain object những không biết khi hủy thì nó tự giải phóng các sprite lưu trong đó không nhỉ?

    ReplyDelete
    Replies
    1. Không có autorelease đâu nên bạn phải thêm code sau vào hàm destruction

      YourClass::~YourClass {
      CC_SAFE_RELEASE_NULL(_yourvector);
      }

      Delete
  4. Cảm ơn bạn đã chia sẻ và hướng dẫn khá tỉ mỉ.
    Chúc bạn luôn thành công :)

    ReplyDelete
  5. bạn ơi, mình cài đặt theo bài 3 của bạn rồi, build được ra thành công file exe trên win rồi nhưng khi build ra android nó hiện lỗi này :
    http://i.imgur.com/KNh1GAp.jpg
    bạn sửa giúp minh với

    ReplyDelete
    Replies
    1. Check lại NDK_ROOT, nhớ là phiên bản r9d nhé, và phải đúng 32, hoặc 64 tùy window.

      Win8 hình như vẫn bị lỗi chưa fix

      Delete
    2. Aa, hóa ra mình dùng bản Win7 32bit nhưng lại lấy cái r9d 64, bạn có bản 32 k gửi cho mình với :(
      Cảm ơn bạn rất nhiều

      Delete
  6. Mình đã down đúng bản r9d 32bit vài cài, khi build ra android nó đã chạy :">
    nhưng sau khi chạy một hồi nó lại hiện ra lỗi mới :( bạn xem giùm mình cái:
    http://i.imgur.com/hZKG6Sn.jpg

    ReplyDelete
    Replies
    1. Chưa cài API 16 ( Android 4.1.2 ) Mở SDK Manager lên ( trong SDK root ) cài thêm API 10 + các loại nếu thích

      Delete
    2. cảm ơn bạn mình đã build thành công file .apk ^ ^
      bây giờ mình muốn thử chạy trên điện thoại thì chỉ cần copy mỗi file .apk đó thôi hay cần gì nữa ạ ?

      Delete
  7. Bạn có code mẫu nào thuộc loại này không?
    " 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"

    ReplyDelete
  8. @Ngoc Quang Tran bạn có thể tham khảo source game Crazy Mario code trên cocos2d-x v2.2 sau: https://github.com/cjlaaa/CrazyMario

    ReplyDelete
  9. Cho mình hỏi mình tải source code trên về rồi build nhưng chỉ có máy bay có physics còn thiên thạch với đạn thì không có mặc dù đã set physics, mình dùng cocos2d-x 3.1.1

    ReplyDelete
  10. sao em copy class với resoure vào thì lại bị lỗi ở bool AppDelegate từ create bị báo lỗi ad

    ReplyDelete
  11. error LNK1120: 2 unresolved externals D:\GAME\projects\diabay\proj.win32\Debug.win32\
    mình bị lỗi như thế này ad xem giúp mình cái

    ReplyDelete
  12. Cho mình hỏi ngốc ngếch chút nha, làm sao để mấy cái vùng đỏ xác định va chạm trên MC và các planets biến mất vậy

    ReplyDelete
    Replies
    1. Đóng comment cho lệnh này

      scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);

      Lệnh này để cho phép xem đường biên của body trong physic

      Delete
  13. có gạn nào chỉnh sửa đc code của game online ko. mình muốn chỉnh sửa code game " hack game " ấy ạ.bác nào giúp đc em xin hậu tạ chu đáo.

    ReplyDelete
  14. quá hay lun, tìm cái này hoài giờ mới thấy, cảm ơn bác nhiều
    You can Download games pc torrent

    ReplyDelete