08 January 2013
Tutorial: Multi-element physics bodies
This week’s tutorial is another regarding Corona’s physics engine — in specific, advanced tactics involving multi-element physics bodies.
First, I should define what a multi-element body is. A multi-element body is a physics body composed of two or more “shapes” to create a whole. It does not define a physical object that you have assembled by attaching several physical objects with weld joints or other joints like a ragdoll. A multi-element body is assembled from several shapes but it’s treated as a unified, solid whole, wherein the individual elements don’t move or flex.
Why multi-element bodies?
This is a rehash for physics veterans, so I’ll be brief. In Box2D, all physical bodies must be drawn with polygonal shapes of eight sides maximum and with no concave angles.
This is fine for a body that you can define in a standard, convex polygon. But what about a body that can’t be traced with only convex angles or can’t be traced accurately in eight sides or less? If you disobey these rules, the collision reaction will be “unpredictable” at best.
The solution is a multi-element body: the basic shape being traced with multiple convex shapes to create a unified body. You should attempt to compose your multi-element bodies using simple shapes and the lowest amount of them.
Part I — Per-element collision control
If you’ve worked with multi-element bodies before, you know that they provide some great capabilities, but they also present some limitations. Let’s step through the capabilities first:
- Individual elements can have unique collision filters. This is useful if you want certain parts of your multi-element body to collide/react with some but not all other physical objects in the world.
- Individual elements can be set as a sensors, allowing all other objects to pass through them while still returning a collision detection event.
- In a collision, each element can return an integer pertaining to the order in which it was declared in the physics.addBody() function — for example, the first element declared will return
1
, the second2
, etc. This allows you to single out which part of a multi-element body is involved in a collision event and take the appropriate action.
Despite these unique capabilities, the following limitations remain:
- Once a collision filter is declared for an element or body, it cannot be changed during Runtime.
- If an element is declared as a sensor, it cannot individually be changed to a non-sensor during Runtime — only the entire body can be swapped between behavior as a sensor or a non-sensor.
Overcoming these limitations
Never fear, today’s tutorial shows you how to overcome both of these limitations. We’ll do this using the physics contact, a feature that I introduced in a previous tutorial. If you didn’t read it already, you can find it here.
To recap the last tutorial, the PhysicsContact allows you to predetermine, via the use of a pre-collision listener, what happens when the collision actually occurs. This allows you to void a collision entirely based on your app logic. We’ll be extending that usage to multi-element bodies in this tutorial.
A possible use-case for this would be a multi-element “space nebula,” for lack of a better phrase (the image above). In the theoretical game, the hero star-fighter must attack each outlying shield pod to destroy them and clear the way to the central nucleus within. A scenario such as this requires a unique approach because the “traditional” methods are prone to these limitations.
- This body cannot be constructed from several smaller bodies and attached by joints, because when an outlying pod is destroyed (and the joints attached to it) the rest of the structure would become physically unstable.
- It’s “all or nothing” when using object.isSensor, so you can’t turn just one destroyed pod into a sensor while ensuring the others retain physical response.
And so, we turn to the PhysicsContact, in conjunction with per-element collision detection, to solve our “destructible shield” issue.
Assembling the nebula
Let’s examine how to create a multi-element body in Corona. We’ll create a 9-element body to trace the nebula. In Corona, we simply do this:
- Display our nebula image on the screen.
- Declare the shapes for the nebula, starting at the top and working around (for our convenience). Note that we must use octagons for the outlying pods because you can’t “offset” a radial shape on a multi-element body. While they’re not as accurate as circles, octagons should suffice for our collision needs.
- Add the physical body and pass each shape to the API in an ordered list of elements. This order must be noted, since it pertains to the integer returned collision detection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
local nebula = display.newImage( "nebula.png" ) nebula.x, nebula.y = display.contentWidth/2, display.contentHeight/2 local podT = { 1,-89, 14,-83, 20,-70, 14,-57, 1,-51, -12,-57, -18,-70, -12,-83 } local beamTR = { 19,-63, 63,-19, 59,-14, 14,-59 } local podR = { 69,-20, 82,-14, 88,-1, 82,12, 69,18, 56,12, 50,-1, 56,-14 } local beamBR = { 19,61, 14,56, 58,13, 62,17 } local podB = { 1,49, 14,55, 20,68, 14,81, 1,87, -12,81, -18,68, -12,55 } local beamBL = { -18,63, -64,17, -59,13, -14,58 } local podL = { -70,-20, -57,-14, -51,-1, -57,12, -70,18, -83,12, -89,-1, -83,-14 } local beamTL = { -18,-65, -14,-61, -59,-15, -64,-20 } physics.addBody( nebula, "dynamic", { shape=podT }, { shape=beamTR }, { shape=podR }, { shape=beamBR }, { shape=podB }, { shape=beamBL }, { shape=podL }, { shape=beamTL }, { radius=24 } -- Radial body used for the nucleus ) local shieldStates = { true, true, true, true, true, true, true, true } |
Additionally, at the end, we must set up a simple shieldStates
table to manage our eight shield objects. This will be used to determine if a particular element is on or off — or to state it another way, this table will track whether a shield element is “intact” or “destroyed” in game logic. We can use a simple non-indexed table of eight boolean values for this.
The basic pre-collision listener
Next, we’ll declare the basic pre-collision listener. As described in the previous tutorial, we must use a pre-collision listener if we intend to utilize the physics contact, because we’ll be telling Corona to manage the collision state immediately before it occurs, not when it occurs.
1 2 3 4 5 6 |
local function nebulaCollide( self,event ) print( event.selfElement ) end nebula.preCollision = nebulaCollide nebula:addEventListener( "preCollision", nebula ) |
This function accomplishes just the basics. Anything that collides with the nebula will return the corresponding integer of that element as event.selfElement
, according to the order in which you declared them. So, because we declared the upper pod as the first element, a collision involving it will return 1
. A collision with the upper-right beam will return 2
, a collision with the right pod will return 3
, and so forth.
Enhancing the pre-collision listener
Now that we know which element of the nebula is involved in a collision, we can mesh this with our shieldStates
table to determine if a collision should occur or not. If the shield element is “destroyed” in our game logic, we can use the physics contact — event.contact
— to instruct Corona to void the collision entirely, making it appear as if that element doesn’t even exist (our ultimate purpose).
1 2 3 4 5 6 7 8 9 10 |
local function nebulaCollide( self,event ) -- Query the position (and state) from "shieldStates" table local isElementIntact = shieldStates[event.selfElement] if ( isElementIntact == false ) then event.contact.isEnabled = false -- Use physics contact to void collision end end |
Managing the shieldStates
table is simple enough. To “destroy” the lower shield pod (fifth position), just code:
1 |
shieldStates[5] = false |
Building on this concept, you can now manage your nebula shields and enact other creative methods, including:
- If a shield pod is destroyed, also destroy the neighboring beams.
- After a certain time, “rebuild” a shield pod and its neighboring beams.
- Manage the health of each pod by expanding upon the
shieldStates
table setup.
As you can see, the physics contact meshed with per-element detection solves a dilemma that isn’t surmountable with traditional methods.
Part II — Multi-element bodies and sensors
Now we’ll discuss a commonly misunderstood aspect of multi-element bodies in regards to sensors.
If you’ve experimented with Corona physics to any degree, you know that a sensor can be a physical body of any legal shape and type (dynamic, kinematic, or static), but it will not react with other bodies in a physical sense, like bouncing.
What’s often misunderstood about multi-element bodies is that every element returns a collision event with a sensor, even though you might assume the body is a whole, unified object from a collision standpoint. This can cause some major confusion if you’re suddenly receiving several “began” phase events as a multi-element body drifts over a sensor, or if you receive an “ended” phase as just one small element drifts back outside the sensor region.
This is actually by design. For example, you might need to sense if just the front wheel of a race car has drifted off the track, while the cockpit remains on the track. However, what’s the solution for sensing if an entire multi-element body is inside or outside a sensor region — say, a “jumping fish” completely exiting a “body of water” defined by a sensor?
Counting the collisions
This jumping fish scenario can be solved by counting the collisions that occur with each element in the fish’s body. We’ll build a table of values for this and name it elementStates
. On the “began” phase, we’ll increase the appropriate count by 1 and on the “ended” phase we’ll decrease it by 1. As noted above, each element will return a collision event with a sensor, so if four sensors overlap an element, its associated count will be 4. If the element then drifts outside three of those sensors, the count will reduce to 1. When an element’s count equals 0, we know that it’s entirely outside the range of all sensors.
We’ll also define a core property named elementsIn
to count how many of the fish’s total elements are either inside or outside the range of all sensors. This value will never exceed 5
for the fish, as it’s a 5-element body. Finally, we’ll define a simple boolean flag named inWater
so we can filter multiple collision reports down to just one for the “completely inside” and “completely outside” states. For our convenience and for efficiency in coding, we’ll define all three of these items as properties of the fish.
Here’s the basic setup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
local fish = display.newImage( "jumpfish.png" ) fish.x, fish.y = display.contentWidth/2, display.contentHeight/2-200 local tail = { -117,12, -123,-46, -68,-13 } local bodyBack = { -89,-26, -61,-39, -20,-46, 20,-49, 42,27, -12,28, -66,16, -94,0 } local bodyFront = { 20,-49, 71,-43, 107,-32, 121,-20, 126,-10, 108,5, 78,19, 43,27 } local finBack = { -39,23, -11,29, -10,41, -32,50 } local finFront = { -9,51, -11,28, 41,27, 15,42 } physics.addBody( fish, "dynamic", { shape=tail }, { shape=bodyBack }, { shape=bodyFront }, { shape=finBack }, { shape=finFront } ) fish.elementStates = { 0,0,0,0,0 } -- Table of per-element collision counts fish.elementsIn = 0 fish.inWater = false |
As you can see, we define the fish’s elements similarly to the nebula in Part I. Additionally, we create the table elementStates
and the properties elementsIn
and inWater
.
Managing the count
The fish requires a standard collision listener, not a pre-collision listener (we’re not accessing the physics contact feature this time).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
local function fishCollide( self,event ) if ( event.phase == "began" ) then if ( self.elementStates[event.selfElement] == 0 ) then self.elementsIn = self.elementsIn+1 end self.elementStates[event.selfElement] = self.elementStates[event.selfElement]+1 elseif ( event.phase == "ended" ) then self.elementStates[event.selfElement] = self.elementStates[event.selfElement]-1 if ( self.elementStates[event.selfElement] == 0 ) then self.elementsIn = self.elementsIn-1 end end if ( self.elementsIn == 0 and self.inWater == true ) then self.inWater = false print( "FISH ENTIRELY OUT OF THE WATER!" ) elseif ( self.elementsIn == 5 and self.inWater == false ) then self.inWater = true print( "FISH ENTIRELY IN THE WATER!" ) end end fish.collision = fishCollide fish:addEventListener( "collision", fish ) |
Now let’s step through the logic:
The began phase:
- First, we check if the colliding element count is
0
. If it is, we know this element is entering a sensor region for the first time and we can safely increase the fish’s totalelementsIn
count by 1. - Next, we increase this specific element’s count by 1.
The ended phase:
- First, we subtract 1 from the colliding element’s count.
- Next, we check if the element’s count is
0
. If it is, we know that it’s entirely outside of all sensor regions, and we reduce the fish’s totalelementsIn
count by 1.
The conditional check:
- First, we check if the fish’s total
elementsIn
count is0
and that it was previously in the water. If both conditions pass, we know the fish has exited the water completely and we set theinWater
flag tofalse
. - For the
elseif
condition, we check if the fish’selementsIn
count is5
and that it wasn’t previously immersed in the water (all elements). If both conditions pass, we know that the fish is entirely in the water and we set theinWater
flag totrue
.
That handles our fish’s sensor collision for both the “entirely inside” and “entirely outside” conditions. This method even works with overlapping and neighboring sensors! For example, ff you’ve constructed your water sensor region with a core body and some “waves” on top, this code will handle all elements of the fish in contact with those sensors.
In summary
That’s it for today’s tutorial. As you’ve learned, mutli-element physics bodies possess some valuable traits that joint-assembled bodies don’t — but they also present some hurdles. Hopefully this tutorial has shown you how to overcome those in your physics-based apps.
David
Posted at 06:48h, 09 JanuaryThis was a very useful post. I really enjoy the more journeyman-to-advanced level blog posts, with lots of examples. Great!
Thanks,
David
Kawika
Posted at 08:41h, 09 JanuaryA accompanying video would be helpful in understanding this concept!
Brent Sorrentino
Posted at 09:21h, 09 JanuaryHi Kawika,
Which part would you like to see? The part about per-element collision, or the part about sensors? I might be able to work up a short video on one of these (or both).
Thanks, Brent
Anshu
Posted at 12:36h, 10 January@Brent
If you don’t mind, here’s my wish-list for the video 🙂
A video tutorial showing:
1) pros-cons of multi-element body approach Vs. joint-assembled body approach. For example, a graphical demo of the two limitations you mentioned in Part 1 (1. This body cannot be constructed from several smaller bodies and attached by joints, because …. AND 2. It’s “all or nothing” when using object.isSensor, so you can’t…) would be perfect
2) A short video on both parts as in .. >>The part about per-element collision, or the part about sensors? I might be able to work up a short video on one of these (or both).<<
Thanks
-Anshu
lblake
Posted at 14:38h, 10 JanuaryHi Brent,
I’d like to see a short video on both examples…
Anshu
Posted at 12:35h, 10 January@Brent
If you don’t mind, here’s my wish-list for the video 🙂
A video tutorial showing:
1) pros-cons of multi-element body approach Vs. joint-assembled body approach. For example, a graphical demo of the two limitations you mentioned in Part 1 (1. This body cannot be constructed from several smaller bodies and attached by joints, because …. AND 2. It’s “all or nothing” when using object.isSensor, so you can’t…) would be perfect
2) A short video on both parts as in .. >>The part about per-element collision, or the part about sensors? I might be able to work up a short video on one of these (or both).<<
Thanks
-Anshu
Adam
Posted at 10:39h, 10 AprilHi,
Great post! Is there a download for the code available please?
Thanks, Adam
Brent Sorrentino
Posted at 10:44h, 10 AprilHi Adam,
Normally I package up a full project for the physics tutorials, but I didn’t for this tutorial (sorry). You can, however, check out the “Physics Contact” demo from the other tutorial, which shares some of the principles and concepts discussed here.
Tutorial:
https://www.coronalabs.com/blog/2012/11/27/introducing-physics-event-contact/
Project:
https://www.dropbox.com/s/4q1y01fvsvv0s3k/PhysicsContact.zip
Thanks,
Brent Sorrentino
Kumar Vyas
Posted at 22:39h, 13 AugustHi,
Can anybody explain me how created the shapes tail,bodyBack,bodyFront,finBack,finFront
local tail = { -117,12, -123,-46, -68,-13 }
local bodyBack = { -89,-26, -61,-39, -20,-46, 20,-49, 42,27, -12,28, -66,16, -94,0 }
local bodyFront = { 20,-49, 71,-43, 107,-32, 121,-20, 126,-10, 108,5, 78,19, 43,27 }
local finBack = { -39,23, -11,29, -10,41, -32,50 }
local finFront = { -9,51, -11,28, 41,27, 15,42 }
I mean how to set those values like -117,12, -123,-46, -68,-13 for tail etc