Tutorial: Continuous actions in Corona

Tutorial: Continuous actions in Corona

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, let’s define our fireLasers() function.  It will create a single bullet and launch it towards its target.

For this implementation, you may create a basic graphic and attach a touch listener. More code will be added to the function later, but for now, let’s create the groundwork:

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:

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 a widget button

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

Note that 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:

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:

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:

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:

In summary

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

Rob Miracle

Rob Miracle creates mobile apps for his own enjoyment and the amusement of others. He serves the Corona Community in the forums, on the blog, and at local events.

  • Lerg
    Posted at 14:18h, 04 February

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

    • Piotr
      Posted at 02:36h, 05 February

      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.

  • J. A. Whye
    Posted at 14:45h, 04 February

    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.


  • Pablo Isidro
    Posted at 16:35h, 05 February

    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.

  • jmp909
    Posted at 06:43h, 07 February

    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?

  • TieLore
    Posted at 10:54h, 08 February

    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.

  • TieLore
    Posted at 10:57h, 08 February

    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.

  • Dave Baxter
    Posted at 05:48h, 11 February

    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 ?


  • Maxim
    Posted at 09:15h, 29 February

    Honestly, I tried your code with sensors, and it is not working. There is a better way to handle “slide off”. Just use corona API. We can use object:setFocus() function. And to work with a multitouch, we need to set touchID.

    Here how it can be. I think this way is much easier.

    if( event.phase == "began" ) then
    display.getCurrentStage():setFocus( event.target, 1 )
    elseif( event.phase == "ended" or event.phase == "cancelled") then
    display.getCurrentStage():setFocus( event.target, nil )

  • Maxim
    Posted at 09:18h, 29 February

    Forgot to say, for different buttons set different id. For example for “left button” set id = 1 and for “right button” set id = 2 and so on.
    display.getCurrentStage():setFocus( event.target, 1 )