Chapter     7

The Game, Part 3: Player Control Input

In the now-classic 1983 movie WarGames, teenaged hacker-extraordinaire (relatively speaking!) David Lightman hacks into the powerful military computer WOPR and accidentally sets off a chain of events that propels the world toward potential nuclear war. In the climactic final scene, David forces WOPR to play a series of tic-tac-toe games against itself to show that some games inevitably lead to no winner (just like nuclear war, get it?).

While great as a conclusion to a movie, a computer playing itself in tic-tac-toe isn’t the most exciting prospect from a purely fun video game perspective. Video games are made for humans to play, whether against the computer (to prove the superiority of mankind?) or against other meat bags. Therefore, a key concept in game development is, naturally enough, getting input from the user.

Astro Rescue is of course no exception: the player needs to be able to pilot the ship, rescue colonists, and escape destruction at the hands (err, tentacles?) of the alien foe. Whether it uses touchscreen control or accelerometer control, it wouldn’t be much of a game without user input—it would just be another closing scene for another 80s movie! So this chapter, while really not that long, is all about this important topic.

Handling Touch Input

As you’ll recall, Astro Rescue allows two modes of user input: touchscreen-only and accelerometer (plus touchscreen) as selected by the player from the settings scene. Touch events and accelerometer events are handled in two separate methods of the gameCore object, and those methods are found in the gameCoreInputEvents.lua file. Let’s start with the touch event handler:

function gc:touch(inEvent)

  if gc.phase ∼= gc.PHASE_FLYING and gc.phase ∼= gc.PHASE_LANDED then
    return;
  end

The only time touch events should be handled when the gameScene is active is if the game is in the flying or landed (sitting on a landing pad) phase. The flying-around part makes sense; obviously the player needs to control the ship, but why when landed, too? Simple: The player needs to be able to take off again! Vertical thrust events need to occur or they’ll be stuck on the ground.

Touch "began" Handling

You’ll be handling touch events on the player’s initial press on the screen in this branch, which makes sense given that thrust is a continual thing as long as he keeps his finger on the screen, so the next line of code we encounter in this method is:

if inEvent.phase == "began" and gc.ship.fuel > 0 then

Recall when you looked at the main game loop that there are flags that indicate which way the ship is thrusting, if any. Those flags will be set in this method, but they only need to be set once because the main game loop will continue to apply movement until the flags are unset; there’s no need to keep updating anything here. Therefore, you only want to do the work here if we’re in the began phase. In addition, the ship can’t thrust if there’s no fuel, so that is factored in as well.

Vertical Thrust

Next, the game needs to determine which of the on-screen controls were actually touched:

if inEvent.target == nil or inEvent.target.controlName == "vertical" then

The second clause in that or logic is obvious: a touch on the vertical thruster control should result in vertical thrust. But what about checking for nil? How does that make sense? Remember that when using accelerometer control, the player will touch the screen to initiate vertical thrust. In that situation, no specific target is tapped because the whole screen is registered as the listener for the touch events. The target attribute of the incoming event object is therefore nil, so you need to check for that and handle it the same way as the explicit vertical thrust arrow control.

gc.ship.thrustVertical = true;

The vertical thrust flag is set to true, so the main game loop will apply that thrust and move the ship accordingly.

if gc.phase == gc.PHASE_LANDED then
  gc.phase = gc.PHASE_FLYING;
end

The other part of the vertical thrust equation is in the case where the ship is on a landing pad already. There, the logic is simple: get ‘em in the air again! The phase is flipped to the flying phase and, as with everything else, the main game loop takes care of the rest.

Left and Right Thrust

Now, what about the left and right thrust controls? Easy enough:

elseif inEvent.target.controlName == "left" and gc.phase ∼= gc.PHASE_LANDED then

As with the vertical thrust, if there’s no gas left then the ship is dead in the water. Unlike the vertical thrust, however, we only allow left and right thrust is the ship is not on a landing pad. If that clause wasn’t part of the logic, then the player could move the ship while on the landing pad and could easily find himself crashing into a wall, so the code won’t allow that. After all, a rocket goes up off a launching pad before it starts moving down range toward its orbit, right?

gc.ship.thrustLeft = true;

The thrustLeft flag gets set and you’re good to go.

Right thrust is handled precisely the same way as left thrust:

elseif inEvent.target.controlName == "right" and gc.phase ∼= gc.PHASE_LANDED then
  gc.ship.thrustRight = true;

end

That also closes out the logic for the began touch event.

Touch "ended" Handling

Next up is what happens when a touch event ends:

elseif inEvent.phase == "ended" then

  if inEvent.target == nil or inEvent.target.controlName == "vertical" then
    gc.ship.thrustVertical = false;
  elseif inEvent.target.controlName == "left" then
    gc.ship.thrustLeft = false;
  elseif inEvent.target.controlName == "right" then
    gc.ship.thrustRight = false;
  end

end

At the end of the day, it’s really nothing but doing the exact opposite of what we do for the began event: setting the flags to false when the player lifts his finger off a specific control element.

When All Is Said and Done . . .

The last step is to deal with audio and animations:

gc:activateDeactivateSoundsAndAnimations();

I’ll describe this method later, but for now just keep in mind that it is responsible for playing (or stopping) the thruster sound as well as starting (or stopping) the correct thruster flame animation sequence for the ship.

Handling Accelerometer Input

When the user wants to play with accelerometer controls, the accelerometer() event handler method comes into play. It’s a surprisingly small bit of code, beginning with this chunk:

function gc:accelerometer(inEvent)

  if gc.phase ∼= gc.PHASE_FLYING then
    return;
  end

As when handling touch input, there’s nothing to do when the ship is on a landing pad. The difference here is that since accelerometer controls left and right movement only, and that movement is only allowed when flying, there’s no need to check specifically if the ship is on a landing pad because even if it is, there’s no work to be done here.

if gc.ship.fuel > 0 then

Once again, the game will only allow movement if the ship has some fuel left, for obvious reasons!

Tilting to the Right

Now, the trick here is to see how far the device is tilted in a given direction. That’s easy to do by interrogating the xInstant or yInstant attributes of the event object, which provide the instantaneous acceleration of the device. In practical terms, that means how far the device is tilted.

There is a trick, though! Have a look at Figure 7-1.

9781430250685_Fig07-01.jpg

Figure 7-1 .  Accelerometer gravity components versus pixel coordinates

You have to remember that Astro Rescue is played in landscape orientation only, as seen on the bottom of Figure 7-1. Notice how the X pixel coordinates always run left to right across the screen, regardless of which orientation the device is held in?

That isn’t true of the two gravity attributes, however. The xInstant attribute always means the tilt of the device, left to right, relative to portrait mode (and yInstant conversely measures tilt up and down, relative to portrait mode). If you rotate the device from portrait to landscape mode, xInstant still measures left and right tilt, and most importantly, it’s still relative to portrait mode.

All this ultimately means is that when in landscape mode, it’s not xInstant we care about, as it would be in portrait mode; it’s yInstant. That attribute still measures the tilt of the device up and down, but that’s relative to portrait mode, which happens to be left and right now, relative to the player.

Once that trick is understood, it’s a simple matter of seeing how far the device is tilted:

if inEvent.yInstant > .2 then
  gc.ship.thrustLeft = true;
  gc.ship.thrustRight = false;

The value of yInstant (and xInstant for that matter) is a value from zero to whatever the maximum the device’s hardware provides. What value to use here is completely a trial-and-error thing: .2 feels about right. It ensures that the ship responds to tilts, but not too much. We don’t want the slight movements the player’s hands subconsciously make to trigger ship thrust, but we don’t want him to have to tilt the device so far that he can no longer see the screen, either. When the yInstant value is positive, that means the device is being tilted to the right (again, relative to the point of view of the player), so thrust comes out from the left.

Tilting to the Left

The only difference between checking for left tilt is that the values returned by yInstant are now negative:

elseif inEvent.yInstant < -.2 then
  gc.ship.thrustLeft = false;
  gc.ship.thrustRight = true;

Otherwise, it is handled the same except that the thrustRight flag is now set to true, of course.

Neutral Tilt

Neutral tilt, or more precisely, the lack of tilt, is the last condition you need to account for.

    else
      gc.ship.thrustLeft = false;
      gc.ship.thrustRight = false;
    end
  end

end

In this case, both flags need to be set to false so the main game loop knows that the ship is not moving anymore; it’s effectively in a neutral state.

Again, Out of the Bullpen to Close It out . . .

As with the touch handler, there’s one last line in the accelerometer handler:

gc:activateDeactivateSoundsAndAnimations();

And wouldn’t you know, that’s the very next thing you need to look at!

Updating Audio and Animations

Once all the flags have been set, whether in the touch handler or the accelerometer handler, the remaining task is to turn the thruster sound on or off and to start the correct animation on the ship. The activateDeactivateSoundsAndAnimations() method is called for precisely that purpose:

function gc:activateDeactivateSoundsAndAnimations()

  if gc.ship.thrustVertical == false and gc.ship.thrustLeft == false and
    gc.ship.thrustRight == false
  then
    if gc.sfx.thrustersChannel ∼= nil then
      audio.stop(gc.sfx.thrustersChannel);
      gc.sfx.thrustersChannel = nil;
    end
  else
    if gc.sfx.thrustersChannel == nil then
      gc.sfx.thrustersChannel = audio.play(gc.sfx.thrusters, { loops = -1 });
    end
  end

The first step is to deal with the thruster sound. It’s a simple enough block of code: if none of the thrust flags are true, then check to see if the thruster sound is playing, as denoted by gc.sfx.thrustersChannel being nil or not. If it’s not, then audio.stop() is called to stop the sound and the reference nilled.

If any of the flags are true, then again you check if the sound is playing already. If it’s not (when gc.sfx.thrustersChannel is nil), then start it playing via audio.play(). Piece of cake!

The next step that needs to be done here is to turn on the correct animation sequence for the ship based on which direction(s) thrust is being applied. To do so, you will need to examine the values of the three thrust flags:

local tV = gc.ship.thrustVertical;
local tL = gc.ship.thrustLeft;
local tR = gc.ship.thrustRight;

There is no technical reason that requires these three variables. In other words, the code would work just fine if instead of tV you used gc.ship.thrustVertical. The only reason I wrote it this way is that it makes the logic checks that follow more concise and easier to read.

Tip  It’s also true, however, that there is a small performance gain from doing this. The Lua interpreter won’t have to go through a longer scope chain lookup in getting the values while determining the outcome of these if statements, since they are all local and so are as close as possible to their usage in terms of scope. This is a good habit to get into, especially when it makes the code less verbose and, in my opinion at least, easier to follow. That being said, this is what you’d call a micro-optimization, and unless it’s in a tight loop, which this isn’t, it likely will have virtually no impact on overall performance. But if nothing else, it’s a very good thing to know and keep in mind in case you do find a performance issue you need to resolve.

Once we have those three variables, we can begin determining which thrust animation to set:

if tV == true and tL == false and tR == false then
  gc.ship.sprite:setSequence("thrustUp");
  gc.ship.sprite:play();

If only the gc.ship.thrustVertical flag, or tV here, is true, then the ship is only thrusting upward.

elseif tV == false and tL == true and tR == false then
  gc.ship.sprite:setSequence("thrustRight");
  gc.ship.sprite:play();

Similarly, gc.ship.thrustLeft (tL) tells us if it’s time to turn on the thrustRight animation.

Note  The flags tell us in what direction thrust is extending out from the ship, while the sequence name tells us which direction the ship is actually moving. Remember that they will always be opposite, so when thrustLeft is true, the flames are coming out from the left side of the ship, pushing it right, so thrustRight is the correct animation sequence.

elseif tV == false and tL == false and tR == true then
  gc.ship.sprite:setSequence("thrustLeft");
  gc.ship.sprite:play();

The same holds true for the gc.ship.thrustRight (tR) flag, which results in the thrustLeft animation being used.

elseif tV == true and tL == true and tR == false then
  gc.ship.sprite:setSequence("thrustUpRight");
  gc.ship.sprite:play();

The ship can also be moving upward and to the right, which means both tV and tL would be true.

elseif tV == true and tL == false and tR == true then
  gc.ship.sprite:setSequence("thrustUpLeft");
  gc.ship.sprite:play();

It can also be moving upward and to the left, of course.

  else
    gc.ship.sprite:setSequence("noThrust");
    gc.ship.sprite:play();
  end

end

Last, if none of the three flags are set, then the ship is not thrusting at all, and is in fact falling toward the ground. In this case there are no flames coming out, which is the noThrust animation sequence.

More on Input Handling with Corona

The user input requirements for Astro Rescue aren’t very intensive, but Corona offers quite a bit more than what you’ve seen in this chapter.

The event object passed into the accelerometer() method, for example, contains a host of other attributes that may be of interest to you:

  • deltaTime tells you how long, in seconds, it has been since the last accelerometer event.
  • isShake will return true if the device was shaken by the user. However, the meaning of the term shake is dependent on the underlying operating system and even the device itself.
  • xGravity and yGravity provide the amount of acceleration due to gravity in the x and y directions, similar to xInstant and yInstant, but xGravity and yGravity are measures over a longer period of time than those two. There are also zInstant and zGravity attributes that measure acceleration for the z axis (toward and away from the user).

There are also a number of additional attributes for touch events that were not used in Astro Rescue, including:

  • time is the time in milliseconds at which the touch event occurred, as measured since the application started.
  • x and y give you the coordinates of the touch event. Related to this is the xStart and yStart attributes, which tell you where the touch began. This is useful in situations where you want to track movement relative to the "began" phase of a touch. For example, say you want to determine if the place where the player lifts his finger is to the right of where he initially put his finger down. You might do something like:
if inEvent.phase == "ended" then
  if inEvent.x > inEvent.xStart then
    print("to the right");
  end
end

Although in a sense tangential to user input, you can also listen for events that deal with device orientation. Events of type "orientation" receive an event object with a delta and type attribute. The delta attribute gives you the number of degrees of difference between the orientation switched from and the orientation switched to. The type attribute returns one of "portrait", "landscapeLeft", "portraitUpsideDown", "landscapeRight", "faceUp", or "faceDown", indicating the new orientation of the device.

The other type of user input often seen in mobile applications nowadays is gyroscope input, at least on devices containing a gyroscope. Many of the same types of attributes for the previously discussed input events are present on the event object for a gyroscope event, such as deltaTime and of course name (which has a value of, unsurprisingly, "gyroscope"). Also provided are xRotation, yRotation , and zRotation attributes, which measure the rate of rotation around a given axis, measured in radians per second.

Summary

In this relatively brief chapter, I showed you the most important aspect (in some ways) of Astro Rescue: user input. You saw how both touch and accelerometer input events are handled, and how the various flags that are used in the main game loop are set based on that input.

You also explored some parts of the Corona API dealing with user input that aren’t actually used in Astro Rescue, to give you a feel for what else you can do with it, including device orientation events and gyroscope input.

In Chapter 8, you’ll look at collision handling. While this is another key element of Astro Rescue (and nearly any video game), it also uses a fairly small amount of code—further testament to the power Corona puts in your hands!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.231.26