Thursday, July 3, 2014

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

Hi cả nhà!

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

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

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

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

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

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

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

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

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

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


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

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

B2 - Dựng màn chơi 

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


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

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

Mở HelloWorldScene.cpp

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

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

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

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

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

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

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

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

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

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

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

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

Hàm tryPopMoles như sau

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

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

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

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

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

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




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

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

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

Dowload Resoucrce, Class

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

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

10 comments:

  1. "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é" thực hiện pack là sao bạn? mình chỉ thấy bạn có bài "Cách xài chùa phầm mềm TexturePacker 3.3.4 vĩnh viễn"

    ReplyDelete
    Replies
    1. Ý mình là dùng soft đó để tạo ra các file resource như trên. Nén nhiều ảnh đơn thành 1 file, giảm code, giảm time, giảm memory...

      Delete
  2. Minh code tren X-code lam theo build ra no bi vay, bi loi gi vay ban oi :3
    https://fbcdn-sphotos-h-a.akamaihd.net/hphotos-ak-xpa1/t31.0-8/10481933_627532977360641_3465118257947459065_o.jpg

    https://scontent-b-mad.xx.fbcdn.net/hphotos-xpf1/t31.0-8/10373132_627532967360642_8821514443640908701_o.jpg

    ReplyDelete
    Replies
    1. ac, không xài Mac, ko biết lỗi kiểu gì, thử nạp 1 ảnh đơn xem có được ko, không thì phải pack lại ảnh = TexturePacker ( có bản dùng cho Mac ) xem thế nào, có thể tọa độ của 2 loại máy nó khác nhau nên bị thế

      Delete
  3. Anh cho em hỏi, cách 1 game cocos2dx nó chạy là như nào vậy ạ? em chỉ biết cop code theo nhưng vẫn chưa hiểu được nó sẽ được bắt đầu chạy từ lớp nào và lần lượt gọi tới các lớp các hàm khác ra sao ạ? Ví dụ như game đập chuột này thì nó bắt đầu chạy từ đâu ạ?

    ReplyDelete
    Replies
    1. File AppDelegate.h chạy đầu tiên, rồi đến AppDelegate.cpp, Trong file .cpp này, bạn muốn gọi đến tệp nào tiếp theo thì gọi qua Phương thức của lớp. Và lớp đó lại gọi đến các lớp tiếp theo. Sau đó thì tùy bạn tổ chức sự liên kết giữa các lớp thôi. Kiểu của OPP ( LT hướng đối tượng ) nói chung là như vậy.

      Delete
  4. mình xin phép chia sẻ đoạn code chọn độ phân giải mà mình đọc được trong sách của nhóm Sonar System
    //check which asset the device requires
    if(2048==screenSize.width || 2048==screenSize.height)//retina ipad
    {
    //Pushing back the resolution directories allows the application to fall back to lower-resolution assets if a higher-resolution asset is missing.
    resDirOrders.push_back("ipadhd");
    resDirOrders.push_back("ipad");
    resDirOrders.push_back("iphone5hd");
    resDirOrders.push_back("iphonehd");
    resDirOrders.push_back("iphone");

    //sets the application's size.
    //độ phân giải màn hình
    glview->setDesignResolutionSize(1536, 2048, ResolutionPolicy::NO_BORDER);
    //The resolution policy underlines how the application handles differences in the application's design size and the screen's physical size. We chose no border, which prevents black borders by zooming in. This might crop some of the background, but it provides the best effect. Games such as Candy Crush Saga use this technique.
    }
    else if(1024==screenSize.width || 1024==screenSize.height)//non retina ipad
    {
    resDirOrders.push_back("ipad");
    resDirOrders.push_back("iphone5hd");
    resDirOrders.push_back("iphonehd");
    resDirOrders.push_back("iphone");

    glview->setDesignResolutionSize(768, 1024, ResolutionPolicy::NO_BORDER);
    } //retina iphone: 5, 5s
    else if(1136==screenSize.width || 1136==screenSize.height)
    {
    resDirOrders.push_back("iphone5hd");
    resDirOrders.push_back("iphonehd");
    resDirOrders.push_back("iphone");

    glview->setDesignResolutionSize(640, 1136, ResolutionPolicy::NO_BORDER);
    } //retina iphone: 4, 4s
    else if(960==screenSize.width || 960==screenSize.height)
    {
    resDirOrders.push_back("iphonehd");
    resDirOrders.push_back("iphone");

    glview->setDesignResolutionSize(640, 960, ResolutionPolicy::NO_BORDER);
    }
    else //non retina and android devices
    {
    //android devices hat have a hight resolution
    if(screenSize.width>1080)
    {
    resDirOrders.push_back("iphonehd");
    resDirOrders.push_back("iphone");

    glview->setDesignResolutionSize(640, 960, ResolutionPolicy::NO_BORDER);
    }
    else//non retina iphone and android devices hat have a lower resolution
    {
    resDirOrders.push_back("iphone");

    glview->setDesignResolutionSize(320, 480, ResolutionPolicy::NO_BORDER);
    }
    }

    ReplyDelete
  5. game này làm trong vb được không

    ReplyDelete