Today’s tutorial introduces a few new physics methods for your bag of tricks. In this tutorial, we’ll solve the “Can I jump?” issue for 2D side-view games and also discuss how to handle sticky projectiles and basic wind tunnel behavior.

All of these methods are included as downloadable projects — please reference the link at the end of this tutorial.

Can I Jump?phystrick-jump

For a 2D game in which a character can jump, you’ll almost always want to ensure that the character can only jump off the ground. Developers tend to approach this differently. One option is to check if the character’s vertical velocity is low enough to indicate that it’s on the ground. Another option, following a jump, is to use a short timer and set a boolean flag so that the character can’t jump again until it returns to the ground. While these are valid approaches, they are more complicated than necessary and they may introduce other jump handling issues.

A cleaner approach is a “foot sensor” attached as a second element to the character’s physics body. As depicted here, this sensor protrudes just a small amount below the character’s base. When this sensor overlaps a ground object, we can assume the character is firmly standing on solid ground and it’s safe to jump. Optionally, we can set the width of the sensor thinner than the character’s overall width to prevent a jump while teetering on the edge of a platform.

Constructing Terrain

The terrain can be constructed quite simply in Corona. In this case, we also add a objType property to all ground objects, so the character can only jump off solid ground, not another horizontal object like a pool of liquid or a pit of spikes.

local cw, ch = display.contentWidth, display.contentHeight
local ground = display.newRect( 0, ch-64, cw, 64 )
ground.objType = "ground"
physics.addBody( ground, "static", { bounce=0.0, friction=0.3 } )

Declaring Character Body

The character can be constructed of two body elements as follows. Notice that the second body element is a sensor, and it’s declared using the polygonal shape method to offset it slightly from the center. Also note that the sensor is declared second in order — this is important as the index order of 2 will be used in the collision detector to check that the sensor collided with the ground.

Notice that we’ve also added a canJump counter to the character to manage its ability to jump. A counter is useful for a specific reason: if you place two ground objects directly beside each other, the foot sensor will briefly overlap both of them as the character moves across the “seam” between them. Using a true/false boolean property instead of a counter would result in unexpected behavior as the sensor exits one ground object and changes to false, even though physically the character is standing on the second ground object and can legally jump. Thus, we can use a counter which increments and decrements as the sensor interacts with one or more ground objects.

local character = display.newRect( 100, 300, 120, 120 ) ; character:setFillColor(0)
physics.addBody( character, "dynamic",
 { density=1.0, friction=0.0, bounce=0.0 },
 { shape={20,0,20,65,-20,65,-20,0}, isSensor=true }
 )
character.isFixedRotation = true
character.canJump = 0

Jump Handler

The jump procedure is simple enough — just check if the canJump counter is greater than 0 and jump.

function touchAction(event)
   if ( event.phase == "began" and character.canJump > 0 ) then
      --jump procedure here
   end
end
stage:addEventListener( "touch", touchAction )

Collision Handler

The final aspect is the collision handler. We just need to check the following:

  1. The colliding body element index is — this indicates the foot sensor has collided, not the character.
  2. The terrain element a ground object — if so, the character can safely jump off it.

Inside this conditional clause, we perform one of the following actions:

  1. On the began phase (foot sensor is entering a ground object) we increment the canJump counter.
  2. On the ended phase (foot sensor is exiting a ground object) we decrement the canJump counter.
function charCollide( self,event )

   if ( event.selfElement == 2 and event.other.objType == "ground" ) then
      if ( event.phase == "began" ) then
         self.canJump = self.canJump+1
      elseif ( event.phase == "ended" ) then
         self.canJump = self.canJump-1
      end
   end
end
character.collision = charCollide ; character:addEventListener( "collision", character )

That’s basically everything required to build an elegant, easy-to-manage method to control jumping off “solid ground,” without the need for timers, velocity checks, or equally complicated methods.

Sticky Projectilesphystrick-proj

Implementing sticky projectiles like arrows or ninja stars is incredibly easy in Corona. In this section we’ll examine:

  1. How to detect the directional velocity to determine if a projectile has enough “speed” to stick to a wall.
  2. How to attach the projectile to another object using a weld joint.

Constructing and launching projectiles is a broad topic, so we won’t go into detail about it. The common aspect to consider is the collision handler function which will handle both cases above.

Our first task is to detect the directional velocity of the projectile on impact with a wall. First, we must calculate the velocity using the Pythagorean theorem. Then, with that value, we can take one of two actions. If the speed is sufficient, we’ll create a weld joint between the projectile and the wall. Otherwise we can do nothing and let the projectile fall to the ground.

Detect Directional Velocity

On collision with a solid object, we first read the projectile’s vx and vy linear velocities. Then we plug those into the formula to get the velocity value we require:

local vx,vy = self:getLinearVelocity()
local dirVel = math.sqrt( (vx*vx)+(vy*vy) )

Make Joint — or Don’t!

Depending on the projectile’s velocity, we’ll perform one of the two actions described above. The required velocity may vary considerably in your own scenario, as it’s dependent on other physical properties and the general threshold above which you’ll allow a projectile to stick.

if ( dirVel > 330 ) then
   self:setLinearVelocity( 0,0 )
   timer.performWithDelay( 10, resolveColl, 1 )
end

In Box2D, some actions cannot be performed in the same time step as a collision because the engine is working out the internal calculations. As indicated above, we need to create the joint after a short timer of 10 milliseconds.

function resolveColl()
   local weldJoint = physics.newJoint( "weld", self, event.other, self.x, self.y )
end

The above code simply creates a weld joint between the projectile and the object it collides with. In Box2D terms, a weld joint is a non-flexing joint which firmly attaches two objects together. However, note that a linear construction of welded objects fused to a static object, as shown in the illustration above, will exhibit a small amount of flex under the effect of gravity or other forces. This is normal behavior. In a scenario under the effect of gravity, we can optionally choose to set “attached” projectiles to a gravityScale of 0, which will remove the force of gravity upon the construction — but not the effect of other forces such as objects that collide with it!

self.gravityScale = 0

And that concludes the basics of the sticky projectiles scenario. Please experiment with various settings and behaviors in the example project, available for download at the end of this tutorial.

Wind Tunnel / Ventsphystrick-wind

Using physics sensors and the :applyForce() function during the Runtime event, it’s relatively easy to create a wind tunnel region or a series of vents which blow objects around. To accomplish this, we’ll assign two properties to both the vents and each object that will interact with them. We’ll call these properties simply xF and yF, representing the x and y force associated with the objects.

The actual vents will be rectangular sensors, rotated to any desired angle. We’ll use that angle to automatically determine the xF and yF force values the vent will apply to a floating object that enters its bounds. In this example, we’ll use leaves as our floating objects.

Each leaf also possesses an xF and yF force property representing the constant force applied to it during the Runtime event. These values will be recalculated when the leaf enters or exits a vent region, and the vector force values will be factored together if/when there are two overlapping vents blowing in different directions. This allows you to set up a “curved wind tunnel” if you wish, by setting up a series of vents intended to guide an object through a curved tunnel.

Configuring Vents

A vent is set up like any standard physics body. Note that we make it a sensor, and we also set a isVent property so that other objects are not considered vents in the collision handler. The fourth line retrieves the proper xF and yF values from the function in the next subsection, depending on the vent’s angle and the “power” that you define as the second argument.

local vent1 = display.newRect( 0, 0, 80, 300 )
physics.addBody(vent1, "kinematic", { isSensor=true } )
vent1.isVent = true ; vent1.rotation = 14 ; vent1.x = 432 ; vent1.y = 660
vent1.xF, vent1.yF = getVentVals( vent1.rotation, 160 )

Retrieve Vent Force Values

The following function accepts two parameters: the vent’s angle and desired power. From these values, it calculates and returns the proper xF and yF force values that will be transferred to any floating object that enters its bounds.

function getVentVals( angle, power )
   local xF = math.cos( (angle-90)*(math.pi/180) ) * power
   local yF = math.sin( (angle-90)*(math.pi/180) ) * power
   return xF,yF
end

Vent Collision Handler

The collision handler performs actions in both the began and ended phases of the collision. In the began phase, it adds the vent’s xF and yF factors to the floating object’s similar properties. In the ended phase, it subtracts them. This allows you to overlap vents as described above to achieve a stacked force effect.

For example, assume that you have one vent pointing directly upward with xF and yF values of 0 and -2 respectively. Then assume another vent — slightly more powerful and facing downward — with xF and yF values of 0 and 3.5 respectively. If you overlap those vents entirely or partially, a floating object which enters the “overlapped” region will undergo an applied force of 1.5 (-2 + 3.5), giving the effect that the vent forces are offset.

function ventCollide( self,event )

   local vent = event.other
   if ( event.phase == "began" and vent.isVent == true ) then
      self.xF = self.xF+vent.xF ; self.yF = self.yF+vent.yF
   elseif ( event.phase == "ended" and vent.isVent == true ) then
      self.xF = self.xF-vent.xF ; self.yF = self.yF-vent.yF
   end
end
leaf1.collision = ventCollide ; leaf1:addEventListener( "collision", leaf1 )

Runtime Force Application

Finally, we need to apply the actual force to each leaf. This is done during a Runtime event, because we want the force values to be cumulative — that is, the longer a leaf stays within the bounds of a vent, the more force is applied to it.

The actual function is simple. If a particular leaf’s xF or yF value is not 0, then the proper force should be applied during each game cycle.

function constantForce()
   if not ( leaf1.xF == 0 and leaf1.yF == 0 ) then
      leaf1:applyForce( leaf1.xF, leaf1.yF, leaf1.x, leaf1.y )
   end
end

Runtime:addEventListener( "enterFrame", constantForce )

Astute observers will correctly point out that the force applied to an object should be less if it’s further from the “source” of the vent. While this is true, applying cumulative force to an object while it’s within the vent bounds creates a convincing effect in most applications.

That’s basically it, aside from thoroughly testing and adjusting the power values assigned to the vents. The final behavior will depend considerably on the mass/density of interacting objects.

In Summary…

To see all three of these examples in action, please download the sample projects. And, as usual, please provide your feedback in the comments section below.

  1. I am glad to see that you posted the ‘foot sensor’ jump technique as that is SOP for most Box2d jumping articles but for some reason most Corona tutorials didn’t use it. Below are two quick notes on some things i learned while working on jumping techniques.

    One benefit for a foot sensor is that you can allow the character jump again a frame or two before the character actually hits the ground. Why is that a benefit? In fast paced games that require a lot of jumping it’s very easy for the user to perceive that they hit the jump button and nothing happened. While we know and can prove that they hit it just a fraction of a second to early the users still feel that the game ‘cheated’. I came across this and extended my foot sensor a little larger then the example above and those complaints went away. It also never looked like the character jumped without hitting the ground.

    There is also a ‘gotcha'(not a bug) for using a boolean flag(true/false) for your canJump flag. The catch is if you have two ground objects seamlessly side by side you will run into a situation where your canJump flag gets set to false even though you are still touching the ground. Here are the order of events that explains how and why this happens.

    1. Player touches ground1, canJump is set to true
    2. Player moves to the end of the first. Before he leaves ground1 his foot touches ground2, which again set canJump to true
    3. Players foot leaves ground1, collision ended event occurs and canJump is set to false.
    4. Since the began event for ground2 collision already happened the canJump flag is stuck at false. The player is now stuck unable to jump(unless a series of other collisions happen).

    The easiest way to fix this is make canJump an integer and use it as a counter. On began do canJump = canJump + 1 and on ended canJump = canJump -1. When you do the jump event just make sure canJump > 0.

    • Brent Sorrentino says:

      Hi @buder,
      Thanks for pointing out the “gotcha” with the foot sensor! I’ve actually used the sensor+counter method in the past, but for some reason it slipped my mind on this tutorial. I just updated the code and content accordingly, plus I modified and re-uploaded the sample project with this change implemented. I appreciate you bringing it to my attention.

  2. About a year ago I worked on a game that needed “wind” (there were “obstacles” like “fans” in the game); I ended up implementing something like the code shown here (albeit with no tutorial available – just my wits and knowledge).

    Due to the nature of the game, I -had- to implement force sensing, based on distance from the fan (IIRC, based on the standard 2D distance calc). I also made the sensor (ie – in the above example “vent1″) a truncated frustum, with the narrow part at the front of the fan, and the longer part further away. This was to simulate more closely how a real fan would work.

    I ended up making the whole thing an object, and adding a way to “point” the fan – so I could ultimately instantiate it anywhere on the screen, pointed in any direction (needed to make various levels in the game). It worked and turned out really well, and I am glad to see the basics of this kind of effect being discussed here, as it made for a great effect for puzzle and obstacle-course/platformer style games.

  3. Hi,
    can I use a “kinematic” sensor colliding with “static” or “kinematic” objects?
    I want to use a laser sight (body is “kinematic” sensor) in my app but it collides only with “dynamic” objects but I need collisions with both “dynamic” and “static”.
    Thanks

    • Brent Sorrentino says:

      Hi Lukas,
      As you can see in your app, one of the bodies (or both) in a collision must be dynamic. I recommend that you set your laser as a *dynamic* sensor. If your scenario is under the influence of gravity, you can prevent the laser from being affected by setting its gravity scale to 0 (myLaser.gravityScale = 0).

      Hope this helps!
      Brent

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>