This guide discusses how to handle collisions between physical objects, or ignore (filter) collisions between specific object sets.
Physics engine collision events are exposed through the standard Corona event listener model with three types:
For general collision detection, you should listen for an event named "collision"
(reference). The "collision"
event includes phases for "began"
and "ended"
which signify the moments of initial contact and broken contact. If you do not implement a "collision"
listener, this event will not fire.
The "preCollision"
event (reference) fires directly before the objects start to interact. This event is often used in conjunction with the physicsContact outlined below.
Note that preCollision events are quite "noisy" and may report many times per contact, potentially affecting performance. Therefore, you should only listen for these events if you truly need "preCollision"
events in the physics world.
The "postCollision"
event (reference) fires directly after the objects have interacted. This is the only event in which the collision force and friction is reported. See the collision force/friction section below for more on this topic. If you do not implement a "postCollision"
listener, this event will not fire.
Like "preCollision"
events, postCollision events are quite "noisy" and may report many times per contact, potentially affecting performance. Therefore, you should only listen for these events if you truly need "postCollision"
events in the physics world.
Some body types
Currently, the Box2D physics engine is liable to crash during a collision if Corona code attempts to modify objects still involved in the collision. This is because Box2D is still working out the iterated mathematics on these objects. However, your collision handler may set a flag or include a time delay via timer.performWithDelay() so that the action can occur in the next application cycle or later.
Complete removal of object(s) via display.remove() or object:removeSelf() can be executed during the same collision event time step, but the following APIs and methods can not be called during a collision event:
Collision events in Corona have a propagation model similar to touch events. You can use this to further optimize your game performance by limiting the number of events that are created. By default, a collision between two objects will fire a local event for the first object, then a local event for the second object, then a global event in the Runtime object, assuming all have active listeners enabled. However, you may only be interested in some of this information.
Any collision event handler that returns true
will stop further propagation of that collision event, even if there are further listeners that would otherwise have received it. This allows you to further limit the number of events that are created and passed to the Lua side. While individual events are not very expensive, large numbers of them can affect overall performance, so limiting event propagation is a good practice.
Collisions are reported between pairs of objects, and they can be detected either locally on an object, using an object listener, or globally using a runtime listener.
When working with Corona display groups and Box2D, it's important to remember that Box2D expects all physics objects to share a global coordinate system. Both grouped and ungrouped display objects will work well since they will share the internal coordinates of that group. However, unexpected results will occur if physical objects are added to different display groups and those groups are moved, scaled, or rotated independently of each other. As a general rule, do not alter the position, scale, or rotation of display groups that contain physics objects. See Physics Notes/Limitations
Local collision handling is best utilized in a self
table ID, representing the object itself, and event.other
which contains the table ID of the other Corona display object involved in the collision. Because Corona display objects behave like Lua tables, you may freely add arbitrary data to these tables such as names, category designators, point values, or even stored functions, and then retrieve this data at collision time. For example, you may wish to store object names in an
local crate1 = display.newImage( "crate.png" ) physics.addBody( crate1, { density=3.0, friction=0.5, bounce=0.3 } ) crate1.myName = "first crate" local crate2 = display.newImage( "crate.png" ) physics.addBody( crate2, { density=3.0, friction=0.5, bounce=0.3 } ) crate2.myName = "second crate" local function onLocalCollision( self, event ) if ( event.phase == "began" ) then print( self.myName .. ": collision began with " .. event.other.myName ) elseif ( event.phase == "ended" ) then print( self.myName .. ": collision ended with " .. event.other.myName ) end end crate1.collision = onLocalCollision crate1:addEventListener( "collision" ) crate2.collision = onLocalCollision crate2:addEventListener( "collision" )
Global collision handling is best utilized in a event.object1
and event.object2
which indicate the references to the two objects involved. Again, you may wish to store each object's name in string format and retrieve it during the collision event.
local crate1 = display.newImage( "crate.png", 100, 200 ) physics.addBody( crate1, { density = 1.0, friction = 0.3, bounce = 0.2 } ) crate1.myName = "first crate" local crate2 = display.newImage( "crate.png", 100, 120 ) physics.addBody( crate2, { density = 1.0, friction = 0.3, bounce = 0.2 } ) crate2.myName = "second crate" local function onGlobalCollision( event ) if ( event.phase == "began" ) then print( "began: " .. event.object1.myName .. " and " .. event.object2.myName ) elseif ( event.phase == "ended" ) then print( "ended: " .. event.object1.myName .. " and " .. event.object2.myName ) end end Runtime:addEventListener( "collision", onGlobalCollision )
When detecting collisions with the global method, there is no way to determine which is the "first" and "second" object involved in the collision. In other words, event.object1
and event.object2
may be crate1
and crate2
respectively, or they might be flipped around. Thus, if you are comparing the two objects in a conditional statement, you may need to build a
Collision events involving a
For a 1
, the second one is number 2
, the third is number 3
, and so on. During a collision event involving at least one
For local collision events, two additional integer values are returned:
event.selfElement
event.otherElement
Similarly, global collision events return two additional values:
event.element1
event.element2
In each case, these values will indicate the index of the body element involved in the collision. For example, assume that one object is a rocket with three body elements: nose, cockpit, and tail. Also assume that the body elements were configured in that specific order: nose first, cockpit second, and tail last. If a collision occurs on its tail, the value of event.selfElement
will be 3
. If a collision occurs on its cockpit, the value of event.selfElement
will be 2
.
Corona includes built-in support for ray casting. This allows you to transmit a
The physicsContact is created by Box2D to manage/override collision behavior in special cases. Typically used in preCollision event detection, this allows you to override certain properties of the collision or even void the collision entirely.
For example, in a platform game, you may wish to construct false
in the preCollision event handler.
The physics contact can also be used to modify the bounce and friction of the collision independently of the body's inherent settings. For instance, using a preCollision event listener, you can conditionally check if two specific object types collide and then either increase or decrease the bounce or friction via event.contact.bounce or event.contact.friction respectively. One actual example of this may be a "pinball" game where the physicsContact could be used to achieve the following:
1
)0
)Once a collision has occurred, you can use postCollision event detection to obtain the direct force of the collision, along with the sideways force between the two objects, which is effectively a frictional force.
The direct force of the collision is reported within the "postCollision"
event as event.force
, and the frictional force is available as event.friction
:
local function onPostCollision( self, event ) if ( event.force > 1.0 ) then print( "force: " .. event.force ) print( "friction: " .. event.friction ) end end object.postCollision = onPostCollision object:addEventListener( "postCollision" )
In the example above, very small forces are conditionally filtered out with event.force > 1.0
"postCollision"
events register a series of increasingly tiny forces while the objects "settle" and it's usually best to ignore force reports below the chosen threshold.
In some cases, you may want to completely void collision interaction between certain objects. For instance, bullets fired from a player should obviously collide with enemies, but there's usually no need to have bullets collide with
This method involves assigning categoryBits
and maskBits
to your objects via a "collision filter" definition, an optional table assigned to the filter
key during body construction. An object will only collide with other objects if their categoryBits
are among its assigned maskBits
. Normally, an object will only have one category bit assigned, but may have one or more mask bits depending on what other things it should collide with.
In the example below, redCollisionFilter
has a maskBits
value of 3
. This filter is applied to the redSquare
object, so redSquare
will only collide with objects that have category bits of 1
or 2
— in this case, that includes other red squares (categoryBits=2
) and the floor (categoryBits=1
). Meanwhile, it will pass through any blue squares (categoryBits=4
), since bit 4
is not included in its maskBits
value.
local floorCollisionFilter = { categoryBits=1, maskBits=6 } -- Floor collides only with 2 and 4 local redCollisionFilter = { categoryBits=2, maskBits=3 } -- Red collides only with 1 and 2 local blueCollisionFilter = { categoryBits=4, maskBits=5 } -- Blue collides only with 1 and 4 local floor = display.newRect( 0, 0, 320, 80 ) physics.addBody( floor, "static", { bounce=0.8, filter=floorCollisionFilter } ) local redSquare = display.newRect( 0, 80, 40, 40 ) redSquare:setFillColor( 1, 0, 0 ) physics.addBody( redSquare, { friction=0, filter=redCollisionFilter } ) local blueSquare = display.newRect( 80, 80, 40, 40 ) blueSquare:setFillColor( 0, 0, 1 ) physics.addBody( blueSquare, { friction=0, filter=blueCollisionFilter } )
Collision filter values can be calculated using a simple
Observe the numbers in the uppermost row: 1
to 512
in binary progression. These "bit" values are the essential aspect in determining collision filter values. For complex games, you can continue past 512
— up to 32768
(16 columns) if necessary — but 10 columns is enough for most games.
Now observe the rows for each object type. Since each object type must have two values for its collision filter categoryBits
and maskBits
)
For each object type in the game, pick one bit number from the uppermost row and mark a circled ⊗ in its associated category
Next, decide if each object type can collide with other objects of the same type. If true, mark an x directly below the ⊗ in the collides with
Next, for each object type, inspect all other object types both above and below its row. If the object type should collide with any other object type, mark an x in the collides with
Note that collision filters work in "reflex" and you must consider all colliding object types in association. For example, asteroids and aliens should collide with bullets, so it may be tempting to mark an x for each of these in the bullet collides with row and then stop the process. However, you must also mark an x in the bullet's bit number column for both asteroids and aliens. If you fail to consider object types in this association, collision filters will not function properly!
When all object types are complete, work through the chart row by row to determine the sum column values. For each 2
and 4
, so the correct sum value is 6
.
When all sum values are calculated, simply set the categoryBits
value for the collision filter to the category sum. Similarly, set the maskBits
value of the filter to the collides with sum. For example, the player object filter with sums of 1
and 6
may be configured as follows:
local playerCollisionFilter = { categoryBits=1, maskBits=6 }
If you add additional game elements later, you must work through the entire chart again
An alternative collision filtering method is to assign a groupIndex
to each object. This value can be a positive or negative integer and it may be a simpler way of specifying collision rules. Objects with the same positive groupIndex
value will always collide with each other, and objects with the same negative groupIndex
value will never collide with each other.
local greenCollisionFilter = { groupIndex = -2 }
If both groupIndex
and collisionBits
/maskBits
are assigned to an object, the groupIndex
has higher precedence.