Debugging chăm chỉ gấp đôi như viết mã ở nơi đầu tiên. Do đó, nếu bạn viết mã rõ ràng đến mức có thể, bạn sẽ, theo định nghĩa, không đủ thông minh để gỡ rối nó. Christopher Thompson
STDIO Tiếp tục loạt bài viết tìm hiểu về thư viện vật lý trò chơi Box2D. Trong bài viết này giới thiệu về collision, bắt sự kiện collision, collision filtering và hiện thực, các cách tác động lên một body và hiện thực, khái niệm user data và sử dụng nó trong Box2D. Các các ví dụ các thành phần được sử dụng Cocos2d-x 3.4.
Nội dung bài viết

Giới thiệu

Trong bài viết Box2D - Phần 1: Giới Thiệu Và Một Số Thuật Ngữ Và Khái Niệm :: www.stdio.vn/articles/read/198/box2d-phan-1-gioi-thieu-va-mot-so-thuat-ngu-va-khai-niem, tôi đã giới thiệu Box2D là gì, các khái niệm cơ bản với Box2D như physics world, body, fixtures. Ở bài viết này tôi sẽ giới thiệu tiếp một số thuật ngữ, khái niệm cơ bản trong Box2D và được thực hiện với Cocos2d-x 3.4.

Tiền đề bài viết

Bài viết nằm trong những loạt bài viết của chương trình Tự Học Cocos2d-x 3.x.x:: www.stdio.vn/programs/content/2/lap-trinh-game-voi-cocos2dx của Stdio.vn :: www.stdio.vn.

Đối tượng hướng đến

Bài viết này tôi hướng đến những lập trình viên đã có kiến thức lập trình games 2D, và chưa có kiến thức tương đương.

Collision

Collision là gì?

Là va chạm của các body trong physics world. Trong dự án games của bạn sẽ sẽ dụng rất nhiều thông tin từ nó để dùng cho logic games của bạn. Ví dụ bạn muốn biết:

  • Khi bắt đầu một va chạm và kết thúc một va chạm.
  • Những điểm trên body va chạm với body khác.
  • Các lực va chạm.
  • ...

Thông tin một collision

Thông tin về một vụ va chạm được chứa trong một đối tượng b2Contact. Từ đây, bạn có thể kiểm tra rằng khi nào va chạm, và tìm hiểu về vị trí và hướng của phản ứng va chạm. Có hai cách chính, bạn có thể nhận được các đối tượng b2Contact từ Box2D. 

Kiểm tra danh sách b2Contact:

for (b2Contact* contact = world->GetContactList(); contact; contact = contact->GetNext())
      contact->... // làm điều gì đó với va chạm này.

Hoặc với một body:

for (b2ContactEdge* edge = body->GetContactList(); edge; edge = edge->next)
      edge->contact->... //làm gì đó với va chạm này.

Contact listeners

Nếu trong dự án games của bạn liên tục xảy ra va chạm. Như bên trên chủ để khi bạn có một va chạm, bạn sẽ phải theo dõi từ lúc bắt đầu cho đến kết thúc va chạm đó, còn bây giờ bạn không còn phải làm điều đó nữa. Một contact listeners là một class (b2ContactListener) với bốn chức năng mà bạn ghi đè lên khi cần thiết.

 void BeginContact(b2Contact* contact);
 void EndContact(b2Contact* contact);
 void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
 void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);

Nhưng đôi khi trong dự án games của bạn, một vài trường hợp bạn phải dùng b2Contact thay cho b2ContactListener. Tôi sẽ nói rõ hơn ở phần sau. Nhưng dù bằng cách nào bạn nhận được những contact listener, chúng đều chứa thông tin tương tự. Phần cơ bản nhất của thông tin là những fixtures của va chạm, bạn lấy được bằng cách:

b2Fixture* a = contact->GetFixtureA();
b2Fixture* b = contact->GetFixtureB();

Chú ý:

  • Từ các fixtuare mà bạn thu được, bạn có thể tìm thấy các body trong vụ va chạm bằng phương thức GetBody() và từ đó bạn sử dụng thông tin này cho logic games của bạn.
  • Khi một sự kiện va chạm xảy ra, tức là có 2 fixture va chạm, nhưng bạn không thể biết chính xác thứ tự của các fixture này. Ví dụ, bạn có một va chạm giữa player và enemi, bạn không thể biết chắc rằng b2Fixture A là của body player hay của là body enemi và ngược lại. 

Collision filtering

Tôi có một ví dụ như sau. Trong dự án games của tôi có 3 loại đối tượng: người chơi, quái vậtcảnh quan.Tôi muốn chúng va chạm theo quy tắc sau:

  • Người chơi không va chạm với người chơi khác, tương tự với con quái vật.
  • Người chơi phải va chạm với con quái vật và ngược lại. Tất nhiên là người chơi và quái vật va chạm với cảnh quan.
  • Các đối tượng canh quan sẽ va chạm với nhau.

Filter catefories và mask

Categoriesmask là điều tốt nhất cho collision filtering, nhưng nó cũng khá phức tạp với những người mới. Mục đích là để xác định thể loại cho mỗi đối tượng và sử dụng mask để lọc các va chạm giữa các đối tượng. Với ví dụ ở trên tôi xác định có 3 loại đối tượng trong va chạm

short CATEGORY_PLAYER = 0x0001; // 0000000000000001 
short CATEGORY_MONSTER = 0x0002; // 0000000000000010
short CATEGORY_SCENERY = 0x0004; // 0000000000000100

Và:

player1FixtureDef.filter.categoryBits = CATEGORY_PLAYER;
player2FixtureDef.filter.categoryBits = CATEGORY_PLAYER;
 
monster1FixtureDef.filter.categoryBits = CATEGORY_MONSTER;
monster2FixtureDef.filter.categoryBits = CATEGORY_MONSTER;
monster3FixtureDef.filter.categoryBits = CATEGORY_MONSTER;
monster4FixtureDef.filter.categoryBits = CATEGORY_MONSTER;
 
groundFixtureDef.filter.categoryBits = CATEGORY_SCENERY;
crate1FixtureDef.filter.categoryBits = CATEGORY_SCENERY;
crate2FixtureDef.filter.categoryBits = CATEGORY_SCENERY;

Chú ý: Ở đây tôi sử dụng các giá trị 0×001, 0×002 và các 0×004. Tại sao không phải 1, 2 và 3? Điều này có vẻ trừu tượng nhưng bạn có thể hiểu đơn giản đó là số lượng phân loại các loại đối tượng . Điều đó có nghĩa rằng các loại có thể là chi hết cho 2 (trong thập phân: 1, 2, 4, 8, 16, 32, 64, 128..., hoặc trong hệ thập lục phân: 0×1, 0×2, 0×4, 0×8, 0×10, 0×20, 0×40, 0×80...). 16 bit có nghĩa là có 16 thư mục có thể, từ 0×0001 để 0×8000.

Bây giờ, hãy xác định các maskBits. Chúng làm việc như sau:

for each object o1 in the world
    for each object o2 in the world
        isCollisionEnabled = (o1.filter.maskBits & o2.filter.collisionBits) ≠ 0

Kết quả của sự va chạm trong ví dụ trên.

short MASK_PLAYER = CATEGORY_MONSTER | CATEGORY_SCENERY; // ~CATEGORY_PLAYER
short MASK_MONSTER = CATEGORY_PLAYER | CATEGORY_SCENERY; // ~CATEGORY_MONSTER
short MASK_SCENERY = -1;
  • Đối tượng oại phong cảnh va chạm với tất cả mọi thứ. Vì vậy, tôi sẽ đặt maskBits là -1 cũng tương đương với 0xFFFF trong lĩnh vực bit.
  • MaskBits có giá trị 0xFFFF (hoặc -1) sẽ cho phép các va chạm với tất cả các đối tượng khác. Nếu bạn thiết lập giá trị của maskBits là 0×0000 (hoặc 0) thì nó sẽ không va chạm với đối tượng nào cả

Đoạn mã ở dưới tôi khởi tạo các giá trị maskBit cho các đối tượng có trên ví dụ trên.:

player1FixtureDef.filter.maskBits = MASK_PLAYER;
player2FixtureDef.filter.maskBits = MASK_PLAYER;
 
monster1FixtureDef.filter.maskBits = MASK_MONSTER;
monster2FixtureDef.filter.maskBits = MASK_MONSTER;
monster3FixtureDef.filter.maskBits = MASK_MONSTER;
monster4FixtureDef.filter.maskBits = MASK_MONSTER;
 
groundFixtureDef.filter.maskBits = MASK_SCENERY;
crate1FixtureDef.filter.maskBits = MASK_SCENERY;
crate2FixtureDef.filter.maskBits = MASK_SCENERY;

Bạn có thể tưởng tượng rằng khi bạn tạo một body, body muốn nói rằng: Tôi là ai (category) và tôi sẽ va chạm với ai (mask).

Filter group

Bạn có thể làm tất cả mọi thứ về va chạm sao cho phù hợp với logic game của bạn với  categoryBits và maskBits. Nhưng đôi khi bạn không cần đến nó vì sự phức tạp về các giá trị bit. Filter group được xây dựng để nhanh chóng tắt hoặc bật kiểm tra va chạm giữa các đối tượng có liên quan bằng cách nào đó. Nó cho phép bạn chỉ định một chỉ số tích phân nhóm. Bạn có thể có tất cả mọi thứ về va chạm với:

  • Chỉ số nhóm giống luôn luôn va chạm (chỉ số tích cực)  
  • Chỉ số nhóm khác nhau không bao giờ va chạm (chỉ số tiêu cực).

Với ví dụ trên tôi sử dụng filter group:

short GROUP_PLAYER = -1;
short GROUP_MONSTER = -2;
short GROUP_SCENERY = 1;

Tôi chỉ định như sau:

player1FixtureDef.filter.groupIndex = GROUP_PLAYER;
player2FixtureDef.filter.groupIndex = GROUP_PLAYER;
 
monster1FixtureDef.filter.groupIndex = GROUP_MONSTER;
monster2FixtureDef.filter.groupIndex = GROUP_MONSTER;
monster3FixtureDef.filter.groupIndex = GROUP_MONSTER;
monster4FixtureDef.filter.groupIndex = GROUP_MONSTER;
 
groundFixtureDef.filter.groupIndex = GROUP_SCENERY;
crate1FixtureDef.filter.groupIndex = GROUP_SCENERY;
crate2FixtureDef.filter.groupIndex = GROUP_SCENERY;

Bạn có thể thấy đoạn mã ngắn hơn rất nhiều. Tuy nhiên, không có cách nào để vô hiệu hóa các va chạm giữa các nhóm, tức là một khi bạn muốn người chơi không va chạm với người chơi khác, nhưng đôi khi có một số trường hợp bạn muốn người chơi khác va chạm với nhau, bạn không thể làm được. Đó là lý do tại sao tôi nghĩ rằng việc sử dụng categoryBits và maskBits lại mạnh hơn so với filter group.

Chú ý:

Quay trở lại với chú ý của mục Collision, tôi đã nói rằng đôi khi việc bạn sử dụng b2Contact thay vì cho b2ContactListeners để bắt một sự kiện va chạm lại tốt hơn. Vì b2ContactListeners không thể bắt được sự kiện va chạm giữa các đối tượng không va chạm với nhau. Có nghĩa là khi một người chơi va chạm với một người khác, chỉ có b2Contact bắt được sự kiện này, còn b2ContactListeners thì không. 

Di chuyển một body

Forces và impulses

Bạn muốn một body và đồng thời body quay tròn. Bạn sẽ muốn sử dụng forces hay impulses cho body đó của bạn. Forces hành động theo thời gian để thay đổi vận tốc của một body trong khi impulses có thể thay đổi vận tốc của body ngay lập tức. Tùy vào mục đích bạn mô phỏng một sự việc nào đó để chọn cách tác động lực lên body cho nó di chuyển.

myBodyDef.type = b2_dynamicBody;
    
b2PolygonShape polygonShape;
polygonShape.SetAsBox(1, 1); 
  
b2FixtureDef myFixtureDef;
myFixtureDef.shape = &polygonShape;
myFixtureDef.density = 1;

myBodyDef.position.Set(20, 20);
bodies = m_world->CreateBody(&myBodyDef);
bodies->CreateFixture(&myFixtureDef);

Với forces:

bodies->ApplyForce(b2Vec2(0,50), bodies->GetWorldCenter(), true);

Với impulses:

bodies->ApplyLinearImpulse(b2Vec2(0,50), bodies->GetWorldCenter(), true);

Với transform:

bodies->SetTransform(b2Vec2(10,20), 0);
  • Với forces và impulses có 2 tham số: lực tác động và điểm tác động trên body. Tùy vào điểm tác động trên body mà sẽ quyết định body đó quay theo chiều cùng với kim đồng hồ hay ngược chiều kim đồng hồ. 
  • Với transform có 2 tham số là: lực tác động và góc quay.

Tôi có đoạn mã sau:

void SetUserData(void* data);
void* GetUserData();
Phân tích:
  • Dòng 1: Điểm tác động của body là trọng tâm của body.
  • Dòng 2: Điểm tác động là góc trên phía bên phải của body.

Linear

Tác động một lực lên body, nhưng body sẽ không đồng thời quay tròn.​

bodies->SetLinearVelocity(b2Vec2(-10.0f, 0.0f));

Với việc sử dụng fores và impulses với điểm tác động là trung tâm body và transform với góc quay là 0 cũng sẽ làm cho body di chuyển mà không đồng thời quay tròn.

Chú ý: Khi bạn tác động một lực lên một body nó luôn là một vecto, có phương và độ lớn tương ứng.

User data

Chức năng này có tác dụng thêm một data vào trong các đối tượng. Các đối tượng gồm

  • b2Body
  • b2Fixture
  • b2Joint

Box2D cần biết thông tin này là gì và nó không làm bất cứ điều gì với data đó. Nó chỉ giữ data và sẽ trả lại cho bạn khi bạn yêu cầu. Đối tượng trên có các phương thức sau:

void SetUserData(void* data);
void* GetUserData();

Thường thì trong games bạn hay sử dụng data là một sprite. Việc bạn thiết lập data cho các body, fixtuare là sẽ vô cùng hữu ích. 

int myInt = 123;
body->SetUserData((void*)myInt);

int udInt = (int)body->GetUserData();
  
//*********************************************************
struct bodyUserData {
      int entityType;
      float health;
      float stunnedTimeout;
};

bodyUserData* myStruct = new bodyUserData;
myStruct->health = 4;//set structure contents as necessary
body->SetUserData(myStruct);

bodyUserData* udStruct = (bodyUserData*)body->GetUserData();

Tổng kết

Với các thuật ngữ và khái niệm cơ bản mới như collision, cách làm cho một body di chuyển, và user data bạn đã có những phần kiến thức cơ bản để áp dụng làm games vật lý với Cocos2d-x. 

Trong bài viết tiếp theo tôi sẽ giới thiệu về thành phần Joint trong thư viện vật lý Box2D Box2D - Phần 3: Joint :: www.stdio.vn/articles/read/291/box2d-phan-3-jointMọi thắc mắc bạn có thể bình luận tại bài viết hoặc liên hệ với Trương Đạt :: www.stdio.vn/users/index/11/truong-dat.

Tham khảo

https://www.cocos2d-x.org

Bạn cần hỗ trợ các dự án kết nối không dây?

Quí doanh nghiệp, cá nhân cần hỗ trợ, hợp tác các dự án IoT, kết nối không dây. Vui lòng liên hệ, hoặc gọi trực tiếp 0942.111912.

  • TỪ KHÓA
  • Arduino
  • ESP32
  • ESP8266
  • Wifi
  • Bluetooth
  • Zigbee
  • Raspberry Pi
THẢO LUẬN
ĐÓNG