Posted on by

In some apps, you’ll need to perform some continuous action while the user’s touch remains on the screen. This could include a character running while the player holds down a “run” button, a space ship firing its lasers while the player’s finger is down, or some action being performed while the player holds the X button on a game controller.

For beginner developers, this process can be elusive because Corona’s event system generates a single event when the screen is touched or a controller button is pressed. Another event is generated when the user lifts off the screen, and of course an event can be generated on each small increment that the touch moves across the screen. However, if the touch remains down and no movement is registered, additional events are not triggered.

To overcome this, let’s explore some techniques to implement continuous actions.

Graphic Button with a Touch Handler

One classic example is a “fire” button in a space shooter where the ship should fire lasers as long as the button is touched.

First, lets define our fireLasers() function.  It will create a single bullet and launch it towards it’s target.

local function fireLasers()
    local blaster = display.newImageRect("laserbeam.png", 8,24)
    blaster.x = ship.x
    blaster.y = ship.y
    transition.to(blaster, {time=1000, y = 0 })
end

For this implementation, you may create a basic graphic and attach a touch listener:

local fireButton = display.newImageRect( "firebutton.png", 64, 64 )
fireButton.x = 50
fireButton.y = display.contentHeight-50

local function handleFireButton( event )
   if ( event.phase == "began" ) then
      -- fire the weapon
      fireLasers()
   elseif ( event.phase == "ended" ) then
      -- stop firing the weapon
      fireLasers()
   end
   return true
end
fireButton:addEventListener( "touch", handleFireButton )

When you touch the button, the “began” phase is triggered and the fireLasers() function is called once (but not repeatedly, since the “began” phase only occurs when the touch begins). Thus, to make the lasers fire constantly, you may create a Runtime enterFrame function and use “flags” to control if it should be firing the lasers or not. Let’s look at the modified code:

local needToFire = false

local function handleEnterFrame( event )
   if ( needToFire == true ) then
      fireLasers()
   end
end
Runtime:addEventListener( "enterFrame", handleEnterFrame )

local fireButton = display.newImageRect( "firebutton.png", 64, 64 )
fireButton.x = 50
fireButton.y = display.contentHeight-50

local function handleFireButton( event )
   if ( event.phase == "began" ) then
      -- fire the weapon
      needToFire = true
   elseif ( event.phase == "ended" and needToFire == true ) then
      -- stop the weapon
      needToFire = false
   end
   return true
end
fireButton:addEventListener( "touch", handleFireButton )

With this code, while the player’s touch remains on the button, the flag needToFire remains true and, in the Runtime listener function, it continuously fires the weapon. Of course, depending on your app’s settings, this is going to occur at a rate of either 30 or 60 times per second, so you should put in some control to limit how fast the actual laser beams fire.

Implementation with widget.newButton()

This is almost identical to the method above, but the button is set up a bit differently:

local needToFire = false

local function handleEnterFrame( event )
   if ( needToFire == true ) then
      fireLasers()
   end
end
Runtime:addEventListener( "enterFrame", handleEnterFrame )

local function handleFireButton( event )
   if ( event.phase == "began" ) then
      -- fire the weapon
      needToFire = true
   elseif ( event.phase == "ended" and needToFire == true ) then
      -- stop firing the weapon
      needToFire = false
   end
   return true
end

local fireButton = widget.newButton{
   width = 64,
   height = 64,
   defaultFile = "firebutton.png",
   overFile = "firebuttonDown.png",
   onEvent = handleFireButton
}
fireButton.x = 50
fireButton.y = display.contentHeight-50

widget.newButton() and its onEvent handler behave almost exactly like the previous example, so it allows you to use the same handleFireButton() function. Alternatively, you could use the onPress and onRelease events instead, but this involves two functions: one to set the needToFire flag and another to stop it:

local needToFire = false

local function handleEnterFrame( event )
   if ( needToFire == true ) then
      fireLasers()
   end
end
Runtime:addEventListener( "enterFrame", handleEnterFrame )

local function startFiring( event )
   needToFire = true
   return true
end

local function stopFiring( event )
   needToFire = false
   return true
end

local fireButton = widget.newButton{
   width = 64,
   height = 64,
   defaultFile = "firebutton.png",
   overFile = "firebuttonDown.png",
   onPress = startFiring,
   onRelease = stopFiring
}
fireButton.x = 50
fireButton.y = display.contentHeight-50

Handling the “Slide Off”

Both of the examples above (graphical button and widget-based button) handle the “began” and “ended” phases of the touch — when the user presses the button, the lasers begin to fire, and when the user lifts off the button, the lasers stop. However, there is a very important case which you must account for: the “slide off” case.

As we’ve discussed, Corona generates an “ended” phase when the user’s touch lifts off the button, but this only occurs if the touch point is actually over the button when the user lifts off. Corona will not generate an “ended” event if the users touches the button and then slides their finger outside of the button content bounds. Thus, unless we account for this, the user could potentially slide outside of the button bounds, lift their finger off, and the lasers would continue firing!

The Solution…

One method of handling the “slide off” case is to place a slightly larger, invisible sensory object behind the button which only senses the “moved” phase. For simplicity in this example, we’ll make it a vector rectangle:

-- Create a rectangle that is 20 pixels larger than the button on all four sides.
local slideOffSensor = display.newRect( 50, display.contentHeight-50, 104, 104 )
-- Make it invisible.
slideOffSensor.isVisible = false
-- IMPORTANT! Invisible objects don't receive touch events unless this property is true.
slideOffSensor.isHitTestable = true

local function handleSlideOff( event )
   if ( event.phase == "moved" and needToFire == true ) then
      -- stop firing the weapon
      needToFire = false
   end
   return true
end

slideOffSensor:addEventListener( "touch", handleSlideOff )

Basically, this invisible object detects any “moved” phase upon it. In most cases, the touch probably began on the button and then slid off the button onto the sensor. This generates a “moved” phase on the rectangle, not a “began” phase. Secondly, this function checks that needToFire is true before toggling it back to false — this handles the alternate possibility that the user touched somewhere outside of the button and then slid onto the sensory rectangle. This case should effectively do nothing, and our conditional handling takes this into account since needToFire will be false unless a press on the button toggles it to true.

There are two additional things to note with this sensory object:

  1. Note that you must set the .isHitTestable property on the object to true. By default, invisible objects do not receive touch/tap events, so you must set this property to true to ensure that the object recognizes touch events.
  2. Remember to place the object behind the button in z-index ordering, either by creating the sensory object before creating the button, or pushing it to the back of its display group via object:toBack().

Using a Game Controller

Game controllers also generate singular events when a button is pressed or released, so you must handle them similarly:

local needToFire = false

local function handleEnterFrame( event )
   if ( needToFire == true ) then
      fireLasers()
   end
end
Runtime:addEventListener( "enterFrame", handleEnterFrame )

local function onKeyEvent( event )
   if ( event.keyName == "buttonX" ) then
      if ( event.phase == "down" ) then
         needToFire = true
      else
         needToFire = false
      end
      return true
   end
   return false
end
Runtime:addEventListener( "key", onKeyEvent )

Timer Versus Runtime Listener

You might find that a timer is easier to work with than a Runtime listener, especially since you’ll almost certainly be limiting the rate of laser fire. Using timers, the code may be refactored like this:

-- Create empty reference to the timer which will be declared later.
local fireTimer

local fireButton = display.newImageRect( "firebutton.png", 64, 64 )
fireButton.x = 50
fireButton.y = display.contentHeight-50

local function handleFireButton( event )
   if ( event.phase == "began" ) then
      -- fire the weapon every 100 milliseconds
      -- third argument of '0' causes the timer to repeat forever (until it's cancelled)
      fireTimer = timer.performWithDelay( 100, fireWeapon, 0 )
   elseif ( event.phase == "ended" ) then
      -- stop the weapon
      timer.cancel( fireTimer )
   end
   return true
end
fireButton:addEventListener( "touch", handleFireButton )

In Summary

Hopefully, this tutorial has provided a foundation for handling continuous actions in Corona SDK. This practice may apply to many scenarios beyond the “laser fire” that we’ve presented. With a little creativity, the sky is the limit!


Posted by . Thanks for reading...

8 Responses to “Tutorial: Continuous Actions in Corona”

  1. Lerg

    Instead of slideOffsensor to handle slide off effect, it’s better to have one of these solutions:
    1. Make event focused, so when the finger goes outside the button, it will still get ended phase.
    2. In moved phase of touch listener of the button calculate bounds of the button and see if current touch position is outside the boundaries to stop firing. When it comes back – start firing again.

    Using timer is better, but if your weapon has low rate of fire by design, people will be able to fire it more often by tapping the button.
    Also to avoid firing lag you should call fireWeapon right after you created the timer (without it the lag in the example is 100ms).

    Reply
    • Piotr

      I agree, slide off is sloppy. Event should be focused if you want that kind of behavior.

      “Using timer is better, but if your weapon has low rate of fire by design, people will be able to fire it more often by tapping the button.”

      Just use internal timer for each weapon e.g in fire method of weapon object. Then you can call fire as much as you want and it won’t fire prematurely.

      Reply
  2. J. A. Whye

    Nice tutorial!

    Another thing to think about is doing something entirely different for your UI instead of a button to shoot (which is kind of the lowest common denominator).

    – Maybe a second touch anywhere on the screen starts firing.

    – Or maybe it’s a toggle, so hit it once and it starts firing and keeps going until you hit the toggle again (and maybe add something so the gun overheats if left on too long, etc.).

    – Maybe it’s a SmartGun with AutoShoot™ capabilities and when turned on will automatically start firing when enemies get within range.

    In other words, while you *can* create buttons that do continuous fire, is that the best way for your game to work? Something to think about when designing your game.

    Jay

    Reply
  3. Pablo Isidro

    Nice! I did something like this technique in the last game I’m creating:

    In that game, one player needs to drag some elements fast while other player is touching and jumping (multitouch). So, since I can’t use setFocus() to solve the problem with the “fast drags”. I had to create a “invisible sensory object” around the elements in order to have a bigger touchable area for that objects.

    Reply
  4. jmp909

    can’t you add the Runtime enterframe listener in the touch began phase and remove it in the touch end / slide off phase?

    or is there no real need?

    Reply
  5. TieLore

    Can’t you just simplify all of this by using the “cancelled” phase on the touch event? I always use this:
    elseif event.phase == “ended” or event.phase == “cancelled” then

    – That seems to work for me. When the user slides off the object, a cancelled event is triggered for that object, unless you set it to the focus, in which case it doesn’t cancel by moving off. But in your example since you don’t set the focus, this would trigger the same as an ended. Which makes your secondary invisible larger object way overkill.
    Or am I wrong on this? Cause, I swear this is what I always use, and last time I checked it works as I’ve outlined.

    Reply
  6. TieLore

    ok, so scratch what I just wrote… I just tested it, and it doesn’t work. But I swear it used to work as I outlined. Either I’m remembering wrong, or it’s been modified since I discovered how it used to work. Either way, it no longer works as I’ve outlined in my last post.

    Reply
  7. Dave Baxter

    When I “slide off” the button it returns to it’s default state visually (like it should), so Corona knows I have slid off the button, so how come it can’t fire a cancelled event ?

    Dave

    Reply

Leave a Reply

  • (Will Not Be Published)