Monday, May 12, 2014

Bài 9 - Làm game đầu tiên - Tạo nhân vật. (Part 1)

Chào mọi người!

Cũng nhanh nhỉ, Blog đã được 14-15 bài rồi đấy. Chắc các bạn cũng đã nắm được một vài kỹ thuật cơ bản về Engine Cocos2d-x V3 này rồi. Với 1 chút kiến thức ấy liệu chúng ta có thể làm được game không. Tất nhiên là được chứ, tuy chỉ là game đơn giản thôi. 

Sau đây mình sẽ hướng dẫn các bạn làm 1 game đơn giản nhất nhé, game này hồi mình mới tìm hiểu về Cocos2dx cũng đã học và làm theo ( bài này nằm trong phần learn, các bạn có thể tìm trên trang chủ, không biết đã được cập nhật chưa. Bài hướng dẫn đó đã cũ rồi, nếu bạn copy code vào V3.0 chắc chắn sẽ ko chạy. Ngoài ra 1 lỗi nữa đó là phần phát hiện Physics quá phức tạp trong bản cũ. V3 sẽ cực kỳ đơn giản. Mình sẽ trình bày lại trong bài của mình nha).

Bài gốc cũ ở đây ( cho Cocos2d-x V2.2.3)

Mô tả 1 chút về game này như sau: Chúng ta có 1 Nhân vật chính và 1 đám quái, Nhân vật chính sẽ tiêu diệt đám quái bằng cách xả đạn về phía quái. Trò chơi có tính điểm đơn giản (mỗi chú nằm xuống được 1 điểm), và có cả màn GameOver, có thể chèn nhạc nền nếu muốn.

GOooooooooooooooooo!

B1 - Tạo Project, thêm Resource ảnh, âm thanh + v..v

Đầu tiên, bạn phải tạo 1 Project mới (bằng cmd nhé). Có một số bạn hỏi sao không tạo = VS2012, hay Eclipse cho trực quan. Mình cũng tìm hiểu rồi, hình như là ko có ( V3.0). Mà thôi, cách quái nào chả được. Dùng dòng lệnh thì trên hệ máy nào ( WIN, MAC, LINUX) cũng đều giống nhau nhé.

>cocos new firstgame -p com.vn.firstgame -l cpp -d f:/android/project

Bạn cần download thê Resource ( file ảnh ) rồi chép đè vào Resource của Project firstgame nhé. À, nếu bạn build cho Android thì chép vào Resource trong thư mục proj.android của firstgame. LINK đây

B2 - Tạo nhân vật

Nghe thì có vẻ ghê ghớm nhỉ, nhưng không có gì ngoài công việc thêm 1 Sprite hình ảnh vào trong Layer của chúng ta thôi. hehe. Các bạn làm như sau

1/ Nhân vật chính
Trong hàm bool HelloWorld::init(), các bạn xóa hết đi, chỉ chừa lại đoạn sau

    if ( !Layer::init() )
    {
        return false;
    }
    
----Xóa
----Xóa

return true;

Sau đó thêm vào trước lệnh return true; đoạn code sau đây ( mình giải thích ý nghĩa luôn )
// Lấy kích thước màn hình
Size winSize = Director::getInstance()->getWinSize(); 
// Tạo 1 Sprite, nhân vật của chúng ta
auto player = Sprite::create("Player.png");
// Đặt lên màn hình ở phía trái
player->setPosition( Point(player->getContentSize().width/2, winSize.height/2) );
// Thêm vào layer nằm trong Scene game
this->addChild(player,1);
// Gọi tới hàm gameLogic , hàm này có nhiệm vụ tạo ra đám quái với thời gian 1 giây 1 quái
this->schedule( schedule_selector(HelloWorld::gameLogic), 1.0 );

2/ Tạo Quái

Bạn mở file HelloWorld.h ra thêm vào đó 3 nguyên mẫu hàm sau

void addTarget();  
void gameLogic(float dt);
void spriteMoveFinished(cocos2d::Node* sender);

Sau đó bạn phải khai báo các hàm này trong HelloWorld.cpp như sau:

*
void HelloWorld::gameLogic(float dt)
{
    this->addTarget();
}

**

// Hàm này tạo ra Quái và di chuyển chúng nè
void HelloWorld::addTarget()
{
    auto target = Sprite::create("Target.png");
    Size winSize = Director::getInstance()->getWinSize();
// Đoạn này tính toán vùng xuất hiện quái sao cho ko bị khuất quái vào viền màn hình

    int minY = target->getContentSize().height/2;
    int maxY = winSize.height
                          -  target->getContentSize().height/2;
    int rangeY = maxY - minY;
    int actualY = ( rand() % rangeY ) + minY;
//
// Đặt quái vào khoảng vị trí trên actualY (random)
    target->setPosition(Point(winSize.width + (target->getContentSize().width/2),actualY));
    this->addChild(target,1);

 //Tính toán tốc độ di chuyển của quái
    int minDuration = (int)2.0;
    int maxDuration = (int)4.0;
    int rangeDuration = maxDuration - minDuration;
    int actualDuration = ( rand() % rangeDuration )
                                        + minDuration;
// Di chuyển quái với 1 tốc độ nằm trong khoảng actualDuration , từ điềm xuất hiện tới điểm Point(0,y)

auto actionMove =  MoveTo::create( (float)actualDuration, Point(0 - target->getContentSize().width/2, actualY) );

// Kết thúc việc di chuyển của quái khi đã tới điểm cuối
 auto actionMoveDone =   CallFuncN::create(CC_CALLBACK_1(HelloWorld::spriteMoveFinished,this));
/
// Chạy 2 Action trên 1 cách tuần tự = lệnh Sequence sau
 target->runAction( Sequence::create(actionMove, actionMoveDone, NULL) );
}

Bạn hãy tham khảo bài về Sprite và các lệnh Action cơ bản ở bài trước nhé.

***
void HelloWorld::spriteMoveFinished(Node* sender)
{
// Hàm này có mỗi công việc là loại bỏ Target ( đang là Sprite) ra khỏi layer của game
// Ép kiểu Contrỏ Sprite của 1 Node*
  auto sprite = (Sprite *)sender;
  this->removeChild(sprite, true);    
}

B3 - Chạy thử
OK men, bây giờ các bạn có thể build và chạy trên window với lệnh sau

>cocos run -s f:android/project/firstgame -p win32

Kết quả đê, ra được thế này là thành công Part 1 nhé




Bạn hoàn toàn có thể build ra apk rồi cài trên Android nhé

>cocos compile -s f:android/project/firstgame -p android --ap 16 ( cho Android 4.1.2 trở lên)

Vậy là hết phần 1 rồi. Đơn giản quá phải không mọi người. Toàn là kiến thức cơ bản thôi mà. Ở bài sau chúng ta sẽ nghiên cứu xem làm thế nào để nhân vật chính bắn ra được viên đạn khi ta chạm vào màn hình, việc phát hiện ra sự kiện chạm màn hình sẽ phải làm như thế nào. Hẹn ở phần sau nhé, sẽ rất thú vị đây


38 comments:

  1. Cảm ơn bạn nhiều nhé ! Bạn ra bài sớm nhé :) Mong đợi bài 10 từng phút, từng giây ")

    ReplyDelete
    Replies
    1. Bạn build ra có chạy không? Mình cũng chưa build lại, hehe

      Cũng sắp ra bài mười rồi, trong thời gian chờ đợi. Hãy ghé qua trang Youtube này để đọc thêm , bổ lắm:

      Trang này của nước ngoài, chứ mình ko quảng cáo cho ai hết à. :-)

      https://www.youtube.com/watch?v=qXqgSNUf9Cc&list=PLRtjMdoYXLf4od_bOKN3WjAPr7snPXzoe

      Delete
  2. chạy ok bạn ạ :) Cảm ơn bạn nhiều nhé

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Ý bạn là khi quái di chuyển đập vào thành màn hình di chuyển ngược lại à?

      À mình chưa biết làm thế nào, để nghiên cứu tiếp.

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Ý bạn là dạng điều khiển à? Nếu vậy thì cho nó MoveTo tới vị trí mình nhấn trên màn hình thôi, còn phức tạp hơn thì phải dùng Steering ( bẻ lái ). Project nào thế?

      Delete
    2. This comment has been removed by the author.

      Delete
  5. // Kết thúc việc di chuyển của quái khi đã tới điểm cuối
    auto actionMoveDone = CallFuncN::create(CC_CALLBACK_1(HelloWorld::spriteMoveFinished,this));
    Bạn có thể giải thích rõ cách sử dụng hàm CC_CALLBACK_1 được không?

    ReplyDelete
    Replies
    1. cái này là Macro, bạn tìm hiểu trong thư viện của nó, để như thế này có khi dễ hiểu hơn việc viết rõ ràng ra đấy, hãy mặc định đi

      Delete
    2. Theo mình biết là: CC_CALLBACK_0/1/2/3 dùng để bạn gọi hàm, Còn 0, 1, 2, 3 là số tham chiếu truyền vào cho cái hàm bạn gọi tới
      Ví dụ hàm HelloWorld::spriteMoveFinished bạn khai báo 3 tham số thì dùng CC_CALLBACK_3

      Delete
  6. Nếu muốn Build game ra nền tảng iOS thì phải làm sao hả bạn? Mình cần có các công cụ gì nữa?

    ReplyDelete
  7. anh ơi, cho em hỏi trong thư mục proj.android của firstgame em ko thấy thư mục Resource nơi, con ngoài thì có

    ReplyDelete
    Replies
    1. Khi build Android, nó sẽ tự động tạo ra thư mục assets chứa Resource

      Build Android phải đọc chú ý này http://laptrinhgamecocos2dx.blogspot.com/2014/04/chu-y-khi-bien-dich-project-cocos2dx.html

      Delete
  8. Bạn giải thích đoạn code này giùm mình:
    this->schedule( schedule_selector(HelloWorld::gameLogic), 1.0 );
    ở đây hàm schedule_selector mình coi trong thư viện thì thấy tham số của nó là một selector nhưng ở đây bạn truyền vào là HelloWorld::gameLogic.Vấn đề ở chỗ này gameLogic mình thấy là một hàm void HelloWorld::gameLogic(float dt) nên mình không hiểu cách bạn gọi lắm. Tại sao có thể gọi HelloWorld::gameLogic được mình đã thử gọi một hàm void khác như HelloWorld::addTarget theo kiểu này thì nó bị lỗi.

    ReplyDelete
  9. addTarget làm gì có tham số thời gian
    hãy xem gameLogic (float dt) dt là thời gian, truyền vào qua this->schedule( schedule_selector(HelloWorld::gameLogic), 1.0 );, dt = 1 giây, cứ 1 giây lại gọi gameLogic và gameLogic lại gọi addTarget = add 1 quái

    Có thể thay this->schedule (xxx) = scheduleUpdate(), và phải bắt buộc thêm hàm update(float dt) dt của hàm update này nằm trong AppDelegate.cpp (

    Bạn hãy hiểu chỗ này là, cứ 1 khoảng thời gian dt ta lại update Scene 1 lần, trong lần update đó Scene sẽ có 1 thay đổi nào đó (ở đây là thêm quái sau 1 giây ). Hãy hiểu ý nghĩa của schedule = lịch biểu, liên quan tới thời gian. schedule_selector là chọn hàm theo lịch biểu thời gian ( hàm có tham số truyền thời gian )

    Bạn nên nghiên cứu kỹ code, và ý nghĩa của hàm 1 chút ( tên tiếng Anh nó thường bao hàm luôn công dụng của hàm ) . nghiên cứu chậm thôi, đừng đi quá nhanh. OK?

    ReplyDelete
    Replies
    1. Cách viết các đoạn code của bạn rất rõ ràng và dễ hiểu rồi mình chỉ không rõ ở phần cú pháp lắm thôi
      this->schedule( schedule_selector(HelloWorld::gameLogic), 1.0 );
      trong HelloWorld.h bạn định nghĩa hàm gameLogic:
      void HelloWorld::gameLogic(float dt)
      {
      this->addTarget();
      }
      Khi muốn sử dụng hàm này trong file HelloWorld.h mình phải viết theo
      HelloWorld::gameLogic(dt) nên mình mới không rõ bạn viết theo cách nào (có thể mình chưa biết). Cách bạn gọi là HelloWorld:gameLogic nên mình mới thắc mắc.

      Delete
    2. Hãy tham khảo ở đây, search "schedule" schedule_selector để hiểu được phần nào

      http://www.cocos2d-x.org/reference/native-cpp/V3.2alpha0/index.html

      this->schedule( schedule_selector(HelloWorld::gameLogic), 1.0 );

      làm 2 việc

      + gọi hàm schedule truyền vào 2 tham số ( 1 là schedule_selector(), 1 là 1.0, cứ 1 giây gọi 1 lần

      + là 1 Macro ( xem Macro trong C, C++ để hiểu cách dùng), Macro này gọi đến hàm gameLogic với thời gian 1.0 giây

      Bạn dùng theo kiểu HelloWorld::gameLogic(dt) , vậy dt bạn lấy đâu ra??

      Hàm schedule được dựng để xử lý theo thời gian cho game bạn nhé, rất hay dùng đấy.. Cũng giống như mấy hàm onTouchBegan, onContactBegin đó, cũng toàn gọi qua Macro.

      Delete
  10. Bạn ơi mình dựa trên cài bài tạo Menu của bạn, giờ mình thêm đoan code này vào để khi nhấn vào New Game thì tao ra quái, nhưng mà nó chỉ tạo có 1 con mà đứng im nữa. Còn nếu mình đưa vào class AppDelagate thì vẫn chạy tốt à. Hình như là nó không có loop thì phải :3
    void HelloWorld::init()
    {
    Scene *scene = Scene::create();
    Layer *layer = HelloWorld::create();
    scene->addChild(layer);
    director->runWithScene(scene);
    this->addChild(scene);
    }

    ReplyDelete
    Replies
    1. Mình nhầm, tức là mình bỏ đoạn code vào playerlayer.cpp, trong hàm init đó

      Delete
    2. Bạn dùng 1 Scene thì phải đảo layer cũ = layer mới, Bạn tạo 1 lớp SceneManager trong đó có hàm đảo Scene ( tạo scene, đảo layer - xem bài menu 7,8). Ở file AppDelege.app thì gọi SceneManager::goMenu();

      Nếu bạn muốn ấn vào menu NewGame, thì bạn xây dựng lớp PlayGame ( có đầy đủ nhân vật và quái ), sau đó khi ấn Newgame thì gọi hàm SceneManager::goNewgame ( tạo 1 layer của lớp PlayGame, sau đó gọi hàm đảo Layer). Hãy xem kỹ lại bài 7-8 và bài 9-10-11-12


      Mà bạn viết cái đoạn này

      Scene *scene = Scene::create();
      Layer *layer = HelloWorld::create();
      scene->addChild(layer);
      director->runWithScene(scene);
      this->addChild(scene);

      Sao khó hiểu thế

      Scene *scene = Scene::create();
      this->addChild(scene);

      hóa ra Layer add Scene à, Mình toàn thấy Scene add Layer làm child thôi, chưa thấy điều ngược lại.

      Delete
  11. mình khai báo lại hàm void addTarget() thành void addTarget(float)
    và sửa câu lệnh this->schedule( schedule_selector(HelloWorld::gameLogic), 1.0 ); thành
    this->schedule( schedule_selector(HelloWorld::addTarget), 1.0 );
    thì thấy game vẫn chạy cùng kết quả..vậy mình bỏ hàm gameLogic đi được không bạn? hay hàm gameLogic còn có tác dụng khác?

    ReplyDelete
    Replies
    1. Được hết, ý nghĩa của nó là cứ 1 giây thêm 1 quái vậy thôi, bạn xây dựng code sao cho mềm dẻo, dễ hiểu, dễ sửa chữa là được

      Delete
  12. This comment has been removed by the author.

    ReplyDelete
  13. mình bị lổi này có ai giúp mình với
    d:\android\project\firstgame\classes\helloworldscene.cpp(34): error C2440: '' : cannot convert from 'const float' to 'cocos2d::Point'

    ReplyDelete
  14. Bạn gửi file helloWorldScene.cpp, mình xem thử

    ReplyDelete
  15. // Đặt lên màn hình ở phía trái
    player->setPosition( Point(player->getContentSize().width/2, winSize.height/2) );
    Bạn có thể giải thích rõ hơn mình câu lệnh này với ! Cảm ơn
    getContenSize lấy kích thức của nhân vật, winsize lấy kích thước màn hình
    @@ Tại sao lại đặt nó vào vị trí bên trái ở giữa @@

    ReplyDelete
    Replies
    1. Bạn xem lại kỹ câu lệnh này, phân tích từng đoạn 1 nhé
      - player->setPosition () là đặt vị trí nhân vật ở đâu đó ( điểm neo của nhân vật thường là Tâm )
      - Point() là tọa độ của 1 điểm trên màn hình
      - player->getContentSize().width/2 có giá trị = 1/2 chiều rộng của nhân vật
      - winSize.height/2 có giá trị = 1/2 chiều cao màn hình
      => Point(x,y) như trên sẽ trả về 1 điểm ở vị trí x = 1/2 chiều rộng nhân vật ( ví dụ 10px) , và y = 1/2 chiều cao màn hình( ví dụ 512px )
      => - player->setPosition (Point(x,y)) , sẽ đăt nhân vật ở phía trái, ở chiều cao ngang màn hình đó

      Delete
  16. Anh ơi. Anh có thể cho em địa chỉ gmail của anh được không ạ?

    ReplyDelete
  17. player->setPosition( Point(player->getContentSize().width/2, winSize.height/2) );lúc buid ra iphone6 bị lỗi exc bad access (code=1, address=0x0) bạn giải hộ mình vs cocos 3.6. thank

    ReplyDelete
  18. Anh cho em hỏi dựa vào tham số nào mà chương trình chỉ loại bỏ đi Target cán đích chứ không loại bỏ các Target khác vậy ạ:
    void HelloWorld::spriteMoveFinished(Node* sender)
    {
    // Hàm này có mỗi công việc là loại bỏ Target ( đang là Sprite) ra khỏi layer của game
    // Ép kiểu Contrỏ Sprite của 1 Node*
    auto sprite = (Sprite *)sender;
    this->removeChild(sprite, true);
    }

    ReplyDelete
  19. e build cho android nó báo lỗi thế này,xử lý thế nào a ơi?
    BUILA FAILED
    D:\ANDROID\SDK\sdk\tools\ant\build.xml:649:The following error occourred while executing this line:
    D:\ANDROID\SDK\sdk\tools\ant\build.xml:694: null returned :1

    ReplyDelete
    Replies
    1. các bài trước e build cho android đều được mà đến bài này nó lại lỗi thế này nên k hiểu. Nếu k debug được chỗ này có khi e bỏ cuộc mất, vì mình làm game ra chủ yếu cho android,mà k build ra cho nó được thì đi tiếp còn ý nghĩa gì nữa :(

      Delete
    2. Import vào Eclipse build sẽ ổn hơn, có lỗi nó sẽ báo cụ thể nhé. Nếu không rõ lỗi gì, copy đoạn báo lỗi, dán lên Google, thường là sẽ có hướng giải quyết

      Delete
  20. This comment has been removed by the author.

    ReplyDelete
  21. Chào bạn!
    Bạn cho mình hỏi là trong hàm actionMoveDone không truyền vào tham số nào hết, làm sao nó biết remove cái gì.
    Cám ơn bạn!

    ReplyDelete