Ok, so you're working on a game and you have some objects that you need to configure how they should (or shouldn't) collide with each other. If it's the first time you're doing this you'll probably end up at the Corona Labs page about collision detection pretty soon. You read through the section "Collision Filtering" and can't help but wonder: Does it really have to be this complicated?
No, it doesn't.
There is absolutely no reason to force knowledge about implementation details like category bits and bit masks onto the developer. All you want to do is specify if objects of type A should collide with objects of type B or not. But to do this, you need to assign unique bit identifiers to each object type, calculate the bit masks yourself, make sure that two colliding object types cross reference each other and so on. It gets really nasty as the number of object types grow.
Consider the example from the Corona Labs documentation in which there are four different object categories: player, asteroid, alien and bullet. The following diagram tries to explain how the bit masks are calculated:
It's actually a quite nice diagram and it does its job of explaining well. But, had there only been a collision filter API written on a slightly higher level it wouldn't be needed at all, because the developer has no business tampering with the bit masks. In code, the filters could be defined as a Lua table like this:
Ok, nothing complicated about this table, but what if:
Instead of assigning categoryBits and calculating maskBits, you just reference the object categories by name and specify if they should collide with any other object categories. No hard coded numbers. Adding new categories or changing existing collision filters require nothing more from the developer than just defining the rules. Understanding which categories collide with each other is as easy as reading the code, no decoding of maskBits into categoryBits needed.
Note also that you don't have to specify the same collision rule twice. For example, both player and asteroid have been configured to collide with alien, so there's no need to also specify that alien should collide with player and asteroid. And for bullet, you don't need to set any filters at all since this has already been configured for the other object types. You CAN explicitly configure both ways if you think that it improves readability, but it's not necessary.
When you're ready to create your objects and need the collision filter for an object category you just request it from CollisionFilter and the categoryBits and maskBits will already be calculated for you.
And finally, here's the full CollisionFilter source code. Hope you like it.
No, it doesn't.
There is absolutely no reason to force knowledge about implementation details like category bits and bit masks onto the developer. All you want to do is specify if objects of type A should collide with objects of type B or not. But to do this, you need to assign unique bit identifiers to each object type, calculate the bit masks yourself, make sure that two colliding object types cross reference each other and so on. It gets really nasty as the number of object types grow.
Consider the example from the Corona Labs documentation in which there are four different object categories: player, asteroid, alien and bullet. The following diagram tries to explain how the bit masks are calculated:
It's actually a quite nice diagram and it does its job of explaining well. But, had there only been a collision filter API written on a slightly higher level it wouldn't be needed at all, because the developer has no business tampering with the bit masks. In code, the filters could be defined as a Lua table like this:
local filters = { player = { categoryBits = 1, maskBits = 6 }, asteroid = { categoryBits = 2, maskBits = 15 }, alien = { categoryBits = 4, maskBits = 15 }, bullet = { categoryBits = 8, maskBits = 6 } }
Ok, nothing complicated about this table, but what if:
- You want to know which objects an asteroid can collide with?
Answer: You need to figure out what the number 15 really means. First of all you need to know that maskBits is actually specifying the categoryBits of the object types to collide with. Second, you need to find out which categoryBits are included in this number? For a number like 15 its pretty easy (8+4+2+1), but what if that number was 175 instead? - You want to change how two object categories interact with each other?
Answer: You need to update the maskBits for both of the object categories you want to change. This means calculating new numbers for the maskBits values. Not very difficult, but still easy to get wrong. - You want to add another object category that should interact with some of the existing ones?
Answer: You'll need to work through the filter settings for all involved object categories and update the maskBits accordingly.
That's a lot of things to think about to solve a very simple problem. To handle these issues I've created a class called CollisionFilter, which gives the developer a simpler way to configure object categories and their collision filters. This is how it would look like to implement the collision matrix above using CollisionFilter:
local CollisionFilter = require("CollisionFilter") local cf = CollisionFilter.new({ "player", "asteroid", "alien", "bullet" }) cf:setCategoryFilter("player", { "asteroid", "alien" }) cf:setCategoryFilter("asteroid", { "asteroid", "alien", "bullet" }) cf:setCategoryFilter("alien", { "alien", "bullet" })
Instead of assigning categoryBits and calculating maskBits, you just reference the object categories by name and specify if they should collide with any other object categories. No hard coded numbers. Adding new categories or changing existing collision filters require nothing more from the developer than just defining the rules. Understanding which categories collide with each other is as easy as reading the code, no decoding of maskBits into categoryBits needed.
Note also that you don't have to specify the same collision rule twice. For example, both player and asteroid have been configured to collide with alien, so there's no need to also specify that alien should collide with player and asteroid. And for bullet, you don't need to set any filters at all since this has already been configured for the other object types. You CAN explicitly configure both ways if you think that it improves readability, but it's not necessary.
When you're ready to create your objects and need the collision filter for an object category you just request it from CollisionFilter and the categoryBits and maskBits will already be calculated for you.
local player = display.newImage( "player.png" ) physics.addBody( player, { filter = cf:getCategoryFilter("player") } )
And finally, here's the full CollisionFilter source code. Hope you like it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
CollisionFilter is a class for Corona SDK that simplifies the setup of collision detection filters. | |
Hides implementation details and removes the need for the developers to work with bit masks. | |
---------------------------- | |
Example usage of CollisionFilter to represent the collision filter worksheet presented at https://docs.coronalabs.com/guide/physics/collisionDetection/index.html: | |
local CollisionFilter = require("CollisionFilter") | |
local cf = CollisionFilter.new({ "player", "asteroid", "alien", "bullet" }) | |
cf:setCategoryFilter("player", { "asteroid", "alien" }) | |
cf:setCategoryFilter("asteroid", { "asteroid", "alien", "bullet" }) | |
cf:setCategoryFilter("alien", { "alien", "bullet" }) | |
local player = display.newImage( "player.png" ) | |
physics.addBody( player, { filter = cf:getCategoryFilter("player") } ) | |
---------------------------- | |
Note! Uses the Bit plugin, so make sure to add the following to your build.settings file: | |
["plugin.bit"] = { | |
publisherId = "com.coronalabs", | |
supportedPlatforms = { iphone=true, android=true, osx=true, win32=true } | |
}, | |
Markus Ranner 2016 | |
--]] | |
local bit = require( "plugin.bit" ) | |
local CollisionFilter = {} | |
CollisionFilter.__index = CollisionFilter | |
--- Private functions -------------- | |
local function createCategories(categoryNames) | |
assert(categoryNames and (#categoryNames > 0), "At least one category name must be specified when creating a new CollisionFilter.") | |
local categories = {} | |
for i = 1, #categoryNames do | |
categories[categoryNames[i]] = { | |
categoryBits = math.pow(2, i - 1), | |
maskBits = 0, | |
} | |
end | |
return categories | |
end | |
--- Public functions -------------- | |
--[[ | |
Setup which categories a specific category should collide with. | |
Param collidesWithCategories is a table of category names, a subset of the categories supplied to the new() constructor. | |
Remember to include categoryName in collidesWithCategories table if the category should be able to collide with itself. | |
No need to setup associative collision filter manually, CollisionFilter handles this for you. | |
For example, if you've already set up "player" to collide with "asteroid", you won't have to specify that "asteroid" should collide with "player" as well. | |
Example: cf:setCollisionFilter("asteroid", { "asteroid", "alien", "bullet" }) | |
See top of class for full example. | |
--]] | |
function CollisionFilter:setCategoryFilter(categoryName, collidesWithCategories) | |
assert(categoryName, "No category name specified") | |
assert(self.categories[categoryName], "Collision filter category has not been defined: " .. categoryName) | |
if (not collidesWithCategories) then | |
collidesWithCategories = {} | |
end | |
-- Go through each of the categories to collide with and set up the bit masks for involved categories | |
for i = 1, #collidesWithCategories do | |
local otherCategoryName = collidesWithCategories[i] | |
assert(self.categories[otherCategoryName], "Collision filter category has not been defined: " .. otherCategoryName) | |
-- Use bitwise OR to set correct bit in bit masks, for both categories so that collision handling is defined correctly both ways | |
self.categories[categoryName].maskBits = bit.bor(self.categories[categoryName].maskBits, self.categories[otherCategoryName].categoryBits) | |
self.categories[otherCategoryName].maskBits = bit.bor(self.categories[otherCategoryName].maskBits, self.categories[categoryName].categoryBits) | |
end | |
end | |
--[[ | |
Returns collision filter table for given categoryName. | |
The returned table can be directly assigned to the filter property of a physics body. | |
Example: physics.addBody(player, { filter = cf:getCollisionFilter("player") }) | |
See top of class for full example. | |
--]] | |
function CollisionFilter:getCategoryFilter(categoryName) | |
if (not self.categories[categoryName]) then | |
error("Collision filter category has not been defined: " .. categoryName) | |
end | |
return self.categories[categoryName] | |
end | |
--[[ | |
categoryName - A table containing all collision filter categories | |
Example: | |
local CollisionFilter = require("CollisionFilter") | |
local cf = CollisionFilter.new({ "player", "asteroid", "alien", "bullet" }) | |
See top of class for full example. | |
--]] | |
function CollisionFilter.new(categoryNames) | |
params = params or {} | |
local newFilter = { | |
categories = createCategories(categoryNames) | |
} | |
setmetatable(newFilter, CollisionFilter) | |
return newFilter | |
end | |
return CollisionFilter |
Comments
Post a Comment