Tutorial: Multi-element physics bodies

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.

shapesThis 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:

  1. 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.
  2. Individual elements can be set as a sensors, allowing all other objects to pass through them while still returning a collision detection event.
  3. 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 second 2, 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:

  1. Once a collision filter is declared for an element or body, it cannot be changed during Runtime.
  2. 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.

nebulaOvercoming 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.

  1. 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.
  2. 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.

nebula-tracedAssembling 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.

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.

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).

Managing the shieldStates table is simple enough. To “destroy” the lower shield pod (fifth position), just code:

Building on this concept, you can now manage your nebula shields and enact other creative methods, including:

  1. If a shield pod is destroyed, also destroy the neighboring beams.
  2. After a certain time, “rebuild” a shield pod and its neighboring beams.
  3. 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

fish-tracedThis 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:

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).

Now let’s step through the logic:

The began phase:

  1. 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 total elementsIn count by 1.
  2. Next, we increase this specific element’s count by 1.

The ended phase:

  1. First, we subtract 1 from the colliding element’s count.
  2. 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 total elementsIn count by 1.

The conditional check:

  1. First, we check if the fish’s total elementsIn count is 0 and that it was previously in the water. If both conditions pass, we know the fish has exited the water completely and we set the inWater flag to false.
  2. For the elseif condition, we check if the fish’s elementsIn count is 5 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 the inWater flag to true.

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.


Tags:
,
Brent Sorrentino
[email protected]

Brent Sorrentino is a full-time Developer Evangelist and technical writer who assists others with game development, code, and in overcoming other challenges to help them bring their app dreams to life.

9 Comments
  • David
    Posted at 06:48h, 09 January

    This 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 January

    A accompanying video would be helpful in understanding this concept!

    • Brent Sorrentino
      Posted at 09:21h, 09 January

      Hi 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 January

        Hi 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 April

    Hi,
    Great post! Is there a download for the code available please?

    Thanks, Adam

  • Kumar Vyas
    Posted at 22:39h, 13 August

    Hi,
    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