Wednesday, May 14, 2014

Bài 11: Làm game đầu tiên - Phát hiện va chạm vật lý ( Part 3 )

Hix!

Không ngờ ra 3 bài này nhanh thật! Mà các bài trong Blog toàn là viết ngẫu hứng, viết 1 lần không cần nháp. Chả bù ngày đi học viết thư cho gái nháp nát cả mấy tờ A4 rồi mới chép lại để gửi ( thảo nào giờ viết cũng có chút lên tay ) : )

Trong 2 phần trước, chúng ta đã xây dựng được 1 nửa project rồi đó, tuy nhiên có 1 phần quan trong nhất cần phải làm trong project này là: tiêu diệt lũ quái bằng những viên đạn bắn ra. Phải làm thế nào đây? Với các phiên bản Engine 1.x, 2.x việc xây dựng các va chạm vật lý giữa các đối tượng là "Hơi bị khoai" do việc tính toán tọa độ, mảng, rồi véc tơ đủ kiểu, rất đau đầu. Tuy nhiên giờ đây trong bản 3 này công việc đó lại rất là đơn giản với chỉ vài dòng lệnh, Nào chúng ta bắt đầu thôi.

Sơ lược công việc trong phần 3 này:
+ Thiết lập thuộc tính vật lý cho các đối tượng ( nhân vật, đạn và quái )
+ Bắt các sự kiện va chạm vật lý xảy ra.
+ Xây dựng hàm xử lý va chạm đó.
Thế thôi nhỉ.

Go Go Go!

B1 - Thiết lập thuộc tính vật lý cho các đối tượng

Các bạn mở file HelloWorldScene.cpp lên, làm các công việc đơn giản sau

Trong hàm createScene(), ta sửa lại lệnh
auto scene = Scene::createScene() thành
auto scene = Scene::createWithPhysics();
// Tạm hiểu là tạo ra 1 Scene - 1 thế giới thu nhỏ có các đăc tính vật lý trong có chứa các đối tượng vật lý.

Thêm 1 lệnh sau
// Lệnh debug này cho phép nhìn thấy các khung body vật lý áp dụng vào các đối tượng ( đường viền đỏ bao quanh đối tượng)
scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
//Thiết lập gia tốc trọng lực bằng 0, để các đối tượng của chúng ta ko rơi xuống đáy màn hình
scene->getPhysicsWorld()->setGravity(Vect(0.0f,0.0f));

1/Nhân vật

Bạn tìm đến dòng this->addChild(player) trong hàm init()

Trên dòng lệnh đó bạn thêm vào đoạn sau

//Tạo 1 bộ khung body vật lý dạng hình tròn
auto playerBody= PhysicsBody::createCircle(player->getContentSize().width / 2);
//Đặt cờ = 1, để kiểm tra đối tượng khi va chạm sau này
player->setTag(1);
//Lệnh này ko hiểu lắm nhưng thực sự ko thể thiếu, bỏ đi sẽ ko có gì xuất hiện khi va chạm
playerBody->setContactTestBitmask(0x1);
//Đặt bộ khung vật lý vào nhân vật
player->setPhysicsBody(playerBody);

2/ Quái

Bạn tìm đến dòng this->addChild(target); trong hàm addTarget()

Trên dòng lệnh đó bạn thêm vào đoạn sau:
// Giải thích giống phần Nhân vật
auto targetBody = PhysicsBody::createCircle(target->getContentSize().width / 2);
target->setTag(2);
targetBody->setContactTestBitmask(0x1);
target->setPhysicsBody(targetBody);

3/ Viên đạn

Trong hàm onTouchEnded bạn tìm dòng projectile->setPosition( Point(20, winSize.height/2) );, 

Thêm vào bên dưới nó đoạn sau, cách giải thích như 2 phần trên

auto projectileBody = PhysicsBody::createCircle(projectile->getContentSize().width / 2);
projectile->setTag(3);
projectileBody->setContactTestBitmask(0x1);
projectile->setPhysicsBody(projectileBody);

Vậy là đã xong việc xây dựng bộ khung vật lý cho các đối tượng

B2 - Bắt sự kiện va chạm giữa các đối tượng

Để bắt sự kiện va chạm này chúng ta chũng tạo ra 1 listener lắng nghe việc va chạm, rồi truyền tới hàm xử lý va chạm thông qua 1 bộ truyền tải, thể hiện bằng code như sau

Trước lệnh return true; của hàm init() thêm vào đoạn code dưới đây:

//Tạo đối tượng lắng nghe va chạm nếu xảy ra
auto contactListener = EventListenerPhysicsContact::create();
//Khi có va chạm sẽ gọi hàm onContactBegin để xử lý va chạm đó, chú ý dòng CC_CALLBACK_1, nhiều tại liệu là CC_CALLBACK_2 sẽ báo lỗi ko chạy
contactListener->onContactBegin = CC_CALLBACK_1(HelloWorld::onContactBegin, this);
//Bộ truyền tải kết nối với đối tượng bắt va chạm
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);

Các bạn chú ý biến _eventDispatcher, biến này có 2 điểm đặc biệt là
+ Chưa từng được khai báo ở đâu ( kể cả trong file tiêu đề HelloWorldScene.h) đáng lẽ phải báo lỗi chứ nhỉ??
+ Biến đó có 1 gạch ở phía đầu, nghĩa là sao??

Bạn hỏi tôi trả lời => Đó là 1 biến đặc biệt có sẵn thuộc 1 lớp ( hình như là lớp Dispatcher ) trong thư viện, nên việc lôi nó ra dùng nó là hoàn toàn bình thường. Còn nếu ko dùng thì bạn phải khai báo 1 thằng khác cũng có chức năng như thế. Chú ý các biến có gạch đầu nhé, thường là biến có sẵn của các lớp.

B3 - Hàm xử lý va chạm

Trong file HelloWorldScene.h thêm vào 1 nguyên mẫu hàm 

//Nhớ là phải khai báo chuẩn như thế này, ko được sai 1 dấu chấm phảy nào nhé ( vì khai báo hàm chồng lên hàm của lớp cha)
bool onContactBegin(const PhysicsContact& contact);

Sau đó ta định nghĩa hàm đó trong HelloWorldScene.cpp như sau

bool HelloWorld::onContactBegin(const PhysicsContact& contact)
{
//Lấy đối tượng va chạm thứ nhất, ép kiểu con trỏ Sprite*
auto bullet = (Sprite*)contact.getShapeA()->getBody()->getNode();
//Lấy giá trị cờ để xét xem đối tượng nào ( đạn, quái, hay nhân vật)
int tag = bullet->getTag();

//Lấy đối tượng va chạm thứ hai, ép kiểu con trỏ Sprite*
auto target = (Sprite*)contact.getShapeB()->getBody()->getNode();
//Lấy giá trị cờ để xét xem đối tượng nào ( đạn, quái, hay nhân vật)
int tag1 = target->getTag();
//Nếu va chạm xảy ra giữa đạn và quái thì xử lý xóa cả đạn và quái khỏi Layer trong Scene ( biến mất khỏi màn)
if((tag==2&tag1==3)||(tag==3&tag1==2))
    {

this->removeChild(bullet,true); // Xóa đạn

this->removeChild(target,true); // Xóa quái
}
// Nếu va chạm xảy ra giữa quái và nhân vật thì NV lăn ra chết , rồi GameOver, rồi tính điểm, cái này để bài sau
if((tag==1&tag1==2)||(tag==2&tag1==1))
        {
// Xử lý GameOver
// Tính điểm
}
    return true; // Phải trả lại giá trị true
}

Vậy là xong, Hãy build, chạy thử và xem kết quả có như mong đợi không nhé.
Kết quả đê, Lỗi luôn mới kinh, mặc dù Code không sai 1 dấu phẩy nào, mình khằng định thế


Nói thêm ở đây, có lẽ mình nên kết luận là 1 Bug nhỏ của Engine. Vì về mặt code không hề lỗi, đã lục tung trên mạng cũng không thấy bài nào hướng dẫn sửa. Trước gặp bài physic này đúng là ức chế vãi. Nhưng cuối cùng cũng tìm được cách, đó là kiếm 1 bài tut Physics rồi so code, thì phát hiện ra file HelloWorldScene.h thiếu đúng 1 dòng lệnh:

USING_NS_CC; ( các bạn copy paste dưới dòng #include "cocos2d.h" trong file HelloWorldScene.h)

Ngay từ khi tạo NewProject thì file HelloWorldScene.h đã ko có dòng này, về sau các bạn tạo Project mới nhớ thêm vào kẻo không biết vì sao lỗi, và không biết sửa ở đâu nhé. và các file ( .h ) các bạn tạo mới cũng nên điền thêm vào nhé.

Kết quả sau khi thêm dòng trên vào, là đây



Kết thúc bài 11 ở đây: Và chúng ta đã biết thêm cách:

+ Tạo 1 Scene chứa các đối tượng có thể tương tác vật lý
+ Tạo khung body vật lý cho các đối tượng
+ Bắt sự kiện va chạm bằng Listener Physics
+ Xây dựng hàm xử lý va chạm onContactBegin

Download file nguồn cho bác nào lười, trong này đã full comment của dòng lệnh

Hẹn gặp lại các bạn trong bài sau


41 comments:

  1. bài này sao run xong nv rơi từ trên xuống rồi biến mất nhưng vẫn bắn đạn đc.quái vẫn chết mà lạ là ko thấy nv đâu?

    ReplyDelete
    Replies
    1. Bạn phải set trọng lực = 0 để quái và nhân vật không rơi xuống
      scene->getPhysicsWorld()->setGravity(Vect(0.0f,0.0f)); ở hàm tạo Scene

      Đạn ko đi cùng nhân vật là do, vị trí đầu của đạn
      projectile->setPosition( Point(20, winSize.height/2) );
      lấy cố định tại Point cố định, nếu Point này bạn lấy vị trí của nhân vật như vầy
      projectile->setPosition(Point(player->getPosition().x,player->getPosition().y));

      thì nhân vật đi đâu, đạn đi đó, bạn xây dựng 1 hàm onTouchMoved để di chuyển nhân vật bằng cách kéo nhân vật trên màn hình nhé.

      Còn vì sao nhân vật, quái lại rơi khỏi màn hình, là vị ta ko tạo 1 đường bao giới hạn quanh màn hình, bạn tạo giống tạo body nhân vật bằng đoạn lệnh này Tham khảo bài physic về 2 quả bóng nhé (Bài 5 và 6 )

      Delete
  2. ok bạn.để mình thử.tks bạn

    ReplyDelete
  3. Lâu ra bài mới thế bạn.... Đợi

    ReplyDelete
    Replies
    1. Haizz, Đâu phải muốn ra bài mới là ra được ngay đâu, cũng phải có thời gian ngâm cứu, tìm hiểu chứ.

      Và mọi người cũng nên chủ động tìm hiểu thêm trên mạng để sau này gặp 1 bài của mình cũng giống như những gì mọi người đã biết thì coi như củng cố kiến thức.

      Nhân đây mình có 1 lời mời này tới mọi người: NẾU BẠN NÀO CÓ KIẾN THỨC VÀ KHẢ NĂNG VIẾT TUTORIAL, và muốn đóng góp bài viết vào Blog này với mình thì ĐĂNG KÝ nhé,

      Rất cám ơn mọi người đã theo dõi và ủng hộ Blog của mình,

      Quả thật là rất muốn mỗi ngày 1 bài nhưng còn nhiều vướng bận: Công việc, kiến thức cũng có giới hạn nữa. hihi.

      Hi vọng là "1 cây làm chẳng lên non, 3 cây chụm lại nên hòn núi cao". Mong nhận được phản hồi của mọi người nhé

      Delete
    2. Cảm ơn bạn ! tiếc là mình chưa đủ khả năng để tham gia với bạn !

      Delete
  4. bool onContactBegin(const PhysicsContact& contact); Bạn ơi sao mình code hàm này toàn bị lỗi à. Có phải kế thừa gì không cậu

    ReplyDelete
    Replies
    1. Bạn bị lỗi như nào, hãy nêu cụ thể thì mình mới biết được chứ

      + Build thành công, khi chạy bị Crash?
      + Build ko thành công? hiện báo lỗi trong Debug?
      + Bạn dùng phiên bản nào 3.0, 3.1 hay 2x?
      + Bạn Copy file nguồn của mình xuống Build cũng lỗi à?

      Delete
    2. mình dùng 2x. Tức là khi mình gọi CCPhysicContact thì không có.

      Delete
    3. Ồ, thì ra thế, thảo nào bị lỗi.

      2X không có tích hợp Physics trong thư viện của nó. Phải tự xây dựng các hàm tính toán tương tác giữa các vật thể, khoai đấy.

      Mình ko nhớ nó có sử dụng Extension Chipmunk, hoặc Box2D hay ko ( xóa mất rồi )

      Bạn nên chuyển sang bản 3x, hay hơn nhiều

      Delete
    4. Cám ơn bạn nhé. :). Mà cài bản 3x có khó không bạn :(

      Delete
    5. Bạn đọc và làm theo bài số 3
      http://laptrinhgamecocos2dx.blogspot.com/2014/04/bai-3-huong-dan-cai-dat-cocos2d-x-v3-tren-win-dow.html?showComment=1401337277515#c7003605874287364479

      Không khó lắm đâu

      Delete
  5. //trong file .h nếu không không USING_NS_CC;
    bool onContactBegin(const cococs2d::PhysicsContact& contact);

    ReplyDelete
    Replies
    1. Nếu ko dùng cái USING_NS_CC;

      Thì với project lớn 1 chút, chắc toàn cococs2d:: nhỉ?

      Delete
  6. Có nhiều usingname mà. Để tránh xung đột hàm thôi. hihi ? Vui ấy mà

    ReplyDelete
  7. Cho mình hỏi vẫn dùng PhysicsBody cho quái or nhân vật nhưng không muốn có cái vòng đỏ đè lên quái vs nhân vật thì làm thế nào hả bạn?

    ReplyDelete
    Replies
    1. B thay chỗ DEBUGDRAW_ALL thành DEBUGDRAW_NONE

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

    ReplyDelete
  9. Cho mình hỏi thêm:Sự kiện Touch. Sao mình cứ phải click+ dê chuột đi một tí nó mới bắt sự kiện nhỉ? Click 1 lần nó không bắt sự kiện?

    ReplyDelete
    Replies
    1. Mình down source code của bạn về lại OK.. Sao lại thế nhỉ?

      Delete
    2. Chắc có gì đó sai sót trong bài

      + Tắt vòng đỏ: tắt scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);

      Delete
  10. this->schedule(schedule_selector(PlayLayer::gameLogic,1000.0));
    Bạn ơi, cái dòng lệnh tạo quái ở trên mình thay đổi thành 1000 hay bất cứ số khác thì vẫn không có gì khác biệt cả. Quái nó ra gần full màn hình luôn :3

    ReplyDelete
    Replies
    1. Mình đã thử, số 1000 kia chính là Time mà, 1000 nghĩa là 1000 giây mới hiện 1 quái. Code bạn có vấn đề rồi. Mình đặt 10 thì đúng 10 giây mới có quái

      Delete
  11. Mình bị nhầm (schedule_selector(PlayLayer::gameLogic),1000 )chứ không phải (schedule_selector(PlayLayer::gameLogic,1000.0))

    ReplyDelete
    Replies
    1. Hix thao nao ra 1 dong quai luon ngay giay dau tien

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

      Delete
  12. va cham 1 lan thi quai va dan deu bien mat . minh muon va cham an 1 dan bien mat . va cham lan 2 quai va dan deu bien mat phai lam sao

    ReplyDelete
    Replies
    1. Phương pháp thì nhiều, nhưng đơn giản nhất là phương pháp gắn cờ, hướng như thế này
      + Tách quái ra thành 1 Class pulic từ Sprite
      + Trong class đó có thêm thuộc tính int hit, phương thức setHit(hit), getHit() chẳng hạn
      + Khi khởi tạo quái thị bạn setHit(1);
      + Va chạm lần bạn làm 2 việc: Xóa đạn
      if getHit() ==1 thì cập nhật lại hit = setHit(getHit() -1 )

      if getHit() ==0 xóa quái


      Đơn giản vậy thôi

      Delete
    2. BAN OI . bay gio minh ko muon click la ban ra dan . minh muon dan tu ban khi linh xuat hien , vay phai xu ly ra sao

      Delete
    3. Quẳng phần bắn đạn vào trong hàm (float dt), sẽ bắn theo thời gian

      Chứ xuất hiện quái rồi bắn thì lấy tọa độ quái < winsize thì bắn cho bắn, nhưng phải bắn thẳng và move player vì nếu bắn theo tọa độ quái thì ko bao giờ trúng ( trừ khi làm đạn tên lửa)

      Delete
  13. bạn giải quyết giúp mình lỗi này với, khi mình bắn 1 loạt đạn ra, thỉnh thoảng 1 viên trúng 2 quái (cùng 1 thơi điểm). thì nó báo lỗi như hình bên dưới, mình nghĩ khi đạn va chạm vs 2 quái 1 lúc thì nó ko biết xét con quái nào để xóa, mong bạn khắc phục lỗi đó giúp mình với
    http://i.imgur.com/Liqq8cR.png
    http://i.imgur.com/GTKp4Ku.png

    ReplyDelete
    Replies
    1. Xác nhận là có lỗi đạn chạm cùng lúc 2 quái thì force stop. Mình xử lý bằng cách này

      auto bullet = (Sprite*)contact.getShapeA()->getBody()->getNode();
      int tag=0;
      if (bullet)
      {
      tag = bullet->getTag();

      auto target = (Sprite*)contact.getShapeB()->getBody()->getNode();
      int tag1=0;
      if (target) {
      tag1 = target->getTag();
      }

      if((tag==2&tag1==3)||(tag==3&tag1==2)){
      bullet->removeFromParentAndCleanup(true);
      target->removeFromParentAndCleanup(true);
      }
      }

      Làm như vậy thì có vẻ đã hết lỗi, ngồi bắn mãi cũng ko lỗi, nhưng khi đạn gặp 2 quái sẽ chỉ có 1 quái chết nhé, 1 thằng coi như được sống

      Tại sao lại bị bug này, bởi vì phép contact chỉ cho chúng ta lấy 2 ShapeA, shapeB, nên khi va chạm giữa 3 đối tượng thì
      + Sau khi xóa 1 quái + xóa 1 đạn, nó sẽ ko thể lấy được shape của đạn để thực hiện contact với quái còn lại => nullpointer ở đây

      Delete
    2. thanks bạn rất nhiều :D

      Delete
  14. if((tag==2&tag1==3)||(tag==3&tag1==2)) Cái đoạn code này là sao thế bạn nhỉ! Mình ko hiểu lắm!

    ReplyDelete
    Replies
    1. Dùng Tag để đại diện cho object mà bạn

      xem lại settag(tag) sẽ hiểu

      Ví dụ

      (tag==2&tag1==3). Nếu đối tượng 1 là đạn, và, đối tượng 2 là quái, thì..... như thế đó

      Delete
  15. Ban đầu mình dùng setCollisionbitmask thay cho setTag của b thì đạn vs quái k va chạm sau m sửa giống của b thì vẫn k dk.Thế là sao nhỉ?

    ReplyDelete
    Replies
    1. Dùng setContactTestBitmask(0x01) thì các đối tượng mới tương tác được. Bản chất là setup bít Flag cho vật thể thôi. 2 Vật thể tương tác khi phép VÀ bít = 1

      Delete
  16. hi, mình cũng mới học cocos, cảm ơn tutorial của bạn rất nhiều.
    Theo mình biết( ko biết có đúng ko).
    một body có 3 thuộc tính cần set,
    targetBody->setCategoryBitmask(0b0010);
    targetBody->setContactTestBitmask(0b0001);
    targetBody->setCollisionBitmask(0b0011);

    BodyA sẽ contact với BodyB khi và chỉ khi (CategoryA And ContactB != 0 và ContactA And CategoryB !=0)
    ở đây And là phép toán And bit ( vd: 0b0011 and 0b1100 = 0)

    còn CC_CALLBACK_2 là 1 macro do cocos define. tùy vào hàm callback có bao nhiêu tham số mà bạn gọi CC_CALLBACK_1 hay CC_CALLBACK_2.
    bạn có thể define them CC_CALLBACK_4
    #define CC_CALLBACK_4(__selector__,__target__, ...) std::bind(&__selector__,__target__, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,std::placeholders::_4, ##__VA_ARGS__)

    ko biết mình hiểu có đúng ko. xin chỉ giáo

    ReplyDelete
    Replies
    1. Về body, set mấy cái đó phục vụ cho việc bắt và xử lý va chạm. Nói đến body bạn phải hình dung ra các tính chất vật lý: khối lượng, ma sát, độ nảy .... trong bài này ko đưa vào

      Còn về xét va chạm thì quan tâm tới 2 cái, Category và CollisionBitmask, Nếu
      if (
      A->Category & B->CollisionBitmask == 1 || B->Category & A->CollisionBitmask == 1
      )

      // If bên trên mình viết kiểu giả mã nhé, đừng ai copy vào project

      {

      Thi xử lý va chạm .

      Các bạn phải chú ý tới dấu & ( 1 dấu & thôi ). Đây là phép AND bit, chứ không phải là && ( điều kiện VÀ ) nhé

      A->Category & B->CollisionBitmask == 1, hoặc có thể == 0 tùy theo bạn đặt bít như nào.

      Khi == 1 thì các đối tượng thực sự va chạm, có xảy ra sự tương tác body ( chạm và thay đổi vị trí, nảy, hoặc ma sát nhau nữa )

      Khi == 0 Các đối tượng vẫn có va chạm nhưng body ko thay đổi gì so với ban đầu ( chúng xuyên qua nhau ) - Tuy nhiên vẫn bắt được va chạm. Dùng trường hợp này khi muốn body Hero không bị xê dịch khi va chạm với Enemy, ví dụ thế ( HEro và Enemy đều có body Dynamic. Hoặc setBody của Hero là Static sẽ ko bị xê dịch.

      Tuy nhiên , Body Static sẽ ko bị trọng lực tác động.

      2 Body cùng là Static sẽ ko va chạm nhau, ko bắt được va chạm

      }


      Delete
  17. Cho mình hỏi là mình code theo giống bạn nhưng sao khi bắn được 3 4 lần
    là bị lỗi "bullet was nullptr"

    ReplyDelete
  18. cho mk hỏi là mình đang làm game bắn tank dùng physic ,vậy làm thế nào để con enemy có thể đi theo đường viền đỏ ở các cạnh vậy mk cocos enemy cho nó chạy nhưng nó ko thể đi thằng theo vạch đỏ

    ReplyDelete