Chapter     9

Wrapping Up

Welcome, my friend, to the final chapter in our exploration of Astro Rescue! You’ve seen a lot so far, gotten quite familiar with a large chunk of Corona SDK, and built a nice little game in the process. There’s only one piece missing now, and that’s the ending scene, the player’s reward for a job well done!

It’s not a particularly lengthy bit of code, but it does introduce a few new concepts, a few new things that Corona has to offer. So, let’s not waste much time with preliminaries—let’s dive right into things.

A Few Variables to Start

The last few chapters have been an exploration of the core code behind Astro Rescue. Of particular interest is to remember that they represent a chunk of code that in a sense piggybacks on a Storyboard API scene, meaning the core game code is decoupled from the scene code.

The benefit of that abstraction is that if you later decided not to use the Storyboard API at all, perhaps instead opting to write your own similar facility, you shouldn’t have to alter the core code much, if at all. The tradeoff of course is a little more complexity (typical of most abstractions in programming).

The benefit of having it all be part of the scene, which is slightly less complexity in terms of less abstraction (vis-à-vis a scene object that then delegates work to functions it calls, possibly in another object), is good for smaller scenes, like the earlier title and menu scenes, and that goes for the ending scene here as well. As such, you’re dealing with just a single source file this time, endingScene.lua, which begins innocuously enough:

local scene = storyboard.newScene();

local endingMusic;

local capturedImage;

You start off with just a few variables defined within the scope of this module. Of course, you have to create a new scene object, something you’ve seen plenty of times before. This scene will have its own music playing, so you’ll define a variable to reference that. Last, you have captureImage, which has to do with making this scene work at all.

Tip  There’s nothing that says you can’t use the decoupled approach while still only having one source file. Simply combine all the gameCore*.lua files into gameScene.lua without changing anything else and that’s what you have. However, I would argue that doing so makes for messy code. Certainly, a source file of that length tends to be a lot more difficult to navigate. As with so many things in Corona, the choice is up to you, but my suggestion is to logically break your code into multiple source files along whatever boundaries make sense, regardless of whether you decouple anything in terms of the architecture of the code.

When I say “making this scene work,” what does that actually mean? When you complete the game to reach the scene, you will be greeted with some cheery music, a final score and a little graphical effect all superimposed over a gradient-filled background. The effect I speak of is, roughly, like a spotlight bouncing back and forth across the text. In Figure 9-1, you can see the scene when the spotlight is pretty much centered on the screen.

9781430250685_Fig09-01.jpg

Figure 9-1 .  Spotlight centered

Over time, the spotlight bounces left and right, highlighting portions of the screen and reducing illumination on the other half. For example, in Figure 9-2, the spotlight is focused on the right side of the screen, leaving part of the left to be dimmed.

9781430250685_Fig09-02.jpg

Figure 9-2 .  Spotlight to the right

Likewise, in Figure 9-3 you can see the spotlight focused on the left side of the screen, leaving the right side in the dark.

9781430250685_Fig09-03.jpg

Figure 9-3 .  Spotlight to the left

Obviously, this is one of those times where the printed page just doesn’t suffice, so if you haven’t done so yet I’d suggest you take the time to play through the game to see this in action. Fortunately, if you’re lazy, like me, then you’ll just hit the thrust up button a few times to get through each level without rescuing any colonists to get to it quickly. Hey, we’re programmers after all, not emergency rescue technicians—we can take a shortcut if we want!

Note  You could, of course, just change the scene that is initially launched in main.lua and get there even quicker . . . but even I have my limits when it comes to being lazy!

The bouncing is a nice, gentle bounce, not a linear movement left and right. That movement, the gradient background, and the spotlight effect are all new Corona capabilities for you to check out!

Creating the Scene

You have to fill in the blanks, so to speak, for the scene’s lifecycle methods now, beginning with createScene( ):

function scene:createScene(inEvent)

utils:log("endingScene", "createScene()");

endingMusic = audio.loadStream(utils:getAudioFilename("endingMusic"));
audio.play(endingMusic, { channel = 2, loops =- 1, fadein = 500 });

A little logging, and then the music immediately starts music playing. The getAudioFilename() method is again used, as it is everywhere you have to load audio resources, to abstract out any platform-specific issues that need to be dealt with. Once it’s loaded, the music begins playing, with a slight fade-in that matches the scene transition time. It plays on a loop, using a specific channel for the same reasons as already discussed when you looked at the title and menu scenes.

Next, you have to create that gradient background:

local bgGradient = display.newRect(
  self.view, 0, 0, display.contentWidth, display.contentHeight
);
bgGradient:setFillColor(
  graphics.newGradient( { 255, 0, 0 }, { 0, 0, 0 } )
);

First, a rectangle is created that is sized to fill the screen. Next, the rectangle is filled, and what is passed to setFillColor() is not RGBA component values, as you saw in the menu scene, but is instead a gradient object created by a call to graphics.newGradient(), in this case a gradient that runs from red to black.

A gradient object can be used to fill many DisplayObjects, including text, but not including circles and rounded rectangles. A gradient has a starting color and an ending color, both of which you specify. Corona then creates all the steps between those two colors automatically. You can specify the starting and ending colors in a number of formats:

  • { gray }: A shortcut for creating a grayscale color.
  • { gray, alpha }: A shortcut for creating a grayscale color that also lets you control opacity.
  • { red, green, blue }: The typical RGB components, as shown in the ending scene code.
  • { red, green, blue, alpha }: RGB components again but now with the additional alpha (opacity) value.

The starting and ending colors are required, and you can then also optionally include a third argument to graphics.newGradient() that specifies the direction of the gradient. The default value, "down", is what is used in the ending scene. You can also specify "up", "left", or "right".

That’s actually all there is to creating gradients with Corona! As I said, you can apply gradients to most things, including text, as you can see here:

local txt1 = display.newText(
  self.view, "Thanks for playing!", 0, 0, native.systemFontBold, 72
);
txt1:setReferencePoint(display.CenterReferencePoint);
txt1.x = display.contentCenterX;
txt1.y = display.contentCenterY - 50;
txt1:setTextColor(
  graphics.newGradient( { 255, 255, 255 }, { 255, 255, 0 } )
);

The first line of text is created just as you’ve seen text created in the past; nothing new or special here. The text is moved off the center of the screen by 50 pixels, and—spoiler alert—the next line of text will be moved down 50 pixels as well, which makes the entire two lines as a whole look to be centered nicely.

Once the text is created, the game creates a gradient to fill it with, this time running from white to yellow. It just makes the text look a little more interesting than the plain white text you saw in the main menu.

The second line of text is created in the same way:

local txt2 = display.newText(
  self.view, "Final Score: " .. gameData.score, 0, 0,
  native.systemFontBold, 72
);
txt2:setReferencePoint(display.CenterReferencePoint);
txt2.x = display.contentCenterX;
txt2.y = display.contentCenterY + 50;
txt2:setTextColor(
  graphics.newGradient( { 255, 255, 255 }, { 255, 255, 0 } )
);

Of course, the final score is shown on this line, but otherwise that and the vertical position are the only differences.

The next step is to ensure that the user will begin a new game next time by clearing all game data:

clearGameData();

That method was explored way back when you looked at main.lua. Just to refresh your memory, it is responsible for resetting the level to 1 and the score to 0, and then saving the game state file so those values are used next time they start a new game (or “continue” the game, which means the same thing at that point).

The next thing you have to add is the code that allows the spotlight effect to work as expected—at least, part of it:

capturedImage = display.captureScreen();
self.view:insert(capturedImage);

Now, you have to imagine the screen at this point in time: the background gradient was created, then the text, so right now the screen is in its final form, minus the spotlight. I’ll jump ahead slightly and tell you that the spotlight effect is subtractive, not additive, meaning that rather than illuminating portions of the screen it’s actually dimming areas, or excluding them from view—masking those parts, if you will.

To make the spotlight effect work you need to capture the complete image first, meaning the screen itself, as it appears without the spotlight in play.

That’s what display.captureScreen() does. It takes a snapshot of the display as it currently exists. Yes, this is how you can take snapshots of your game! It gives you back a DisplayObject, with which you can then do whatever you want: anything you can do to a DisplayObject generally, including saving it to a file. To do so, pass true to display.captureScreen(), which actually saves it to your device’s photo album as a JPEG file (or as a PNG if running in the simulator).

Note  To be able to save a snapshot like this on Android, you need to add the android.permission.WRITE_EXTERNAL_STORAGE permission to your build.settings file.

When you call display.captureScreen(), you get a DisplayObject back as mentioned. This also means that, in the usual fashion with DisplayObjects created after others, it is on top of everything else currently on the screen.

Therefore, after this line of code executes, you have one screen-sized DisplayObject that has the background gradient and the two lines of text, but those individual objects are not children of it! It simply appears that way—they have the individual background rectangle with the gradient, and the two text objects are still there, but now obscured by the full-screen DisplayObject.

That leads to the next step in this process:

bgGradient:removeSelf();
bgGradient = nil;
txt1:removeSelf();
txt1 = nil;
txt2:removeSelf();
txt2 = nil;

This removes the original objects drawn to the screen, since you no longer need them.

Turning the Spotlight On

Now it’s time to create that spotlight effect. To do so, you’re going to make use of Corona’s bit mask functionality. First, the code:

capturedImage:setMask(graphics.newMask("circlemask.png"));
capturedImage.maskDir = 1;
capturedImage.maskXNext = (display.contentWidth / 3);
capturedImage.maskXDir = 1;
capturedImage.maskScaleX = 4;
capturedImage.maskScaleY = 4;

Looks simple enough, right? I’ll break it down, starting with the first line, and more specifically, the call to graphics.newMask().

A bit mask, which is what graphics.newMask() creates, is a structure that allows you to “mask off” portions of an image. A bit mask is created from a mask image, such as circlemask.png used here, which you can see in Figure 9-4.

9781430250685_Fig09-04.jpg

Figure 9-4 .  circlemask.png

The image is converted to grayscale internally by Corona, or it may already be grayscale, as is the case with circlemask.png. Any pixels in the mask image that are white will be transparent, any that are black are opaque, and any that are in between will have a varying degree of opacity. All you do then is apply the mask to a DisplayObject via a call to its setMask() method, which you can conceptualize as the mask image being superimposed over the target image. Then, the mask does its thing: any pixel in the mask image that is not black will allow the corresponding pixel in the target image to be visible, to varying degrees based on the level of gray of the pixel. Likewise, any pixel that is black in the mask image will cause the corresponding pixel in the target image to be blocked from view.

Note  A bit mask image must follow two rules: its width and height must be even multiples of 4, and it must have a black border around it of 3 or more pixels in width.

By default, the mask image will be centered on the image it is applied to, which just so happens to be exactly what you want here. Just as important for our purposes here is the fact that the maskScaleX and maskScaleY attributes allow you to scale the mask. If you didn’t do that, what you’d see is a fairly tiny little circle in the middle of the screen just kind of bouncing left and right. It wouldn’t look so hot. By scaling it up, it largely fills the screen and only the edges around the circle, the black portions in the mask image, get obscured as the spotlight moves.

Get That Spotlight Moving!

Speaking of the spotlight moving, that’s accomplished with our old friend the Transition API, and the fact that a mask can be moved, and that its x/y location is relative to the DisplayObject to which it is applied.

capturedImage.transition = function()
  capturedImage.tween = transition.to(capturedImage, {
    time = 2000, maskX = capturedImage.maskXNext,
    transition = easing.inOutQuad,
    onComplete = function()
      if capturedImage.maskXDir == 1 then
        capturedImage.maskXDir = 2;
        capturedImage.maskXNext = -(display.contentWidth / 3);
        capturedImage.transition();
      else
        capturedImage.maskXDir = 1;
        capturedImage.maskXNext = (display.contentWidth / 3);
        capturedImage.transition();
      end
    end
  });
end;

The maskDir, maskXNext, and maskXDir attributes, which are custom attributes added to the mask object that I intentionally didn’t mention earlier when you saw the mask being created, are used to make the transition work. The maskDir attribute tells you which direction the mask is currently moving in. When it’s a value of 1, the mask is moving right to left. When it’s a 2, it’s moving left to right.

We create a tween over a 2-second period of time that moves the mask from whatever its current location is (centered on the captured image initially) to the value of maskXNext. The value maskXNext is initially set to, (display.contentWidth / 3), results in the mask moving to the left of center a bit. When the tween completes, the direction is reversed and the mask is set to move to the right, a bit off-center.

The other trick here is the used of easing.inOutQuad as the transition easing function. Easing in general was discussed in Chapter 4, but as a refresher, it allows you to specify that the motion of a tween isn’t linear but instead gives the impression of forces like friction and gravity affecting an object as it moves. In the case of the spotlight, it gradually slows down as it reaches its outer position (whether left or right of center), stops, and then begins moving in the other direction, only to repeat the slowdown as it reaches the other extreme. It’s a nice, subtle little effect that looks a lot better than just a straight left-to-right motion at a constant velocity.

Of course, none of this would matter if not for:

capturedImage.transition();

That kicks off the animation and gets the spotlight moving.

As amazing as it seems, that’s more or less all the code behind this scene! It doesn’t take much to produce some relatively impressive things with Corona, something I hope by this point you’ve seen for yourself a number of times. However, even if most of what makes this scene what it is has been explored already, there are in fact a few more bits of code to look at.

Other Scene Methods

The remainder of the methods in this scene are all old friends, starting with willEnterScene():

function scene:willEnterScene(inEvent)

  utils:log("endingScene", "willEnterScene()");

end

Okay, admittedly, not much to see there! In enterScene(), which is next, there’s only slightly more:

function scene:enterScene(inEvent)

  utils:log("endingScene", "enterScene()");

  Runtime:addEventListener("touch", scene);

end

The user can tap anywhere on the screen to exit this scene and return to the main menu, so you naturally need a touch event handler attached to the Runtime object, again calling the touch() method of the scene object.

What about exiting the scene, you ask? The exitScene() method is here for that, too:

function scene:exitScene(inEvent)

  utils:log("endingScene", "exitScene()");

  Runtime:removeEventListener("touch", scene);

  audio.stop(2);

  transition.cancel(capturedImage.tween);

end

Once the touch event listener is removed, the music is stopped. Once again, since this music was played on a specific channel, you need to stop that specific channel, unlike the sound effects during gameplay.

You also need to stop the animation of the spotlight. Since that’s just a basic tween created with the Transition API, the transition.cancel() method does the trick, passing the reference to the tween stored on the capturedImage DisplayObject as a custom attribute.

There is also a didExitScene() handler, in case it’s needed later, but for now it’s not needed and is therefore just an empty “do-nothing” method.

function scene:didExitScene(inEvent)

  utils:log("endingScene", "didExitScene()");

end

Last, destroyScene() is present to clean up when the scene is destroyed by the Storyboard API and the title scene is shown again:

function scene:destroyScene(inEvent)

  utils:log("endingScene", "destroyScene()");

  audio.dispose(endingMusic);
  endingMusic = nil;

  capturedImage:removeSelf();
  capturedImage = nil;

end

This only leaves the endingMusic and the capturedImage DisplayObject to clean up, so once you use the appropriate API methods to remove them and nil out their references, you’re good to go.

Touch Events

The touch handled method that was set up in enterScene() is the next method encountered as you walk through this source file.

function scene:touch(inEvent)

  utils:log("endingScene", "touch()");

  if inEvent.phase == "ended" then
    utils:log("endingScene", "Going to menuScene");
    storyboard.gotoScene("menuScene", "zoomOutIn", 500);
  end

  return true;

end

It’s a standard touch handler, nothing special, that triggers a storyboard.gotoScene() call to transition back to the menu scene at the behest of the user.

Kicking It All Off

The only thing left to do, having fully defined the scene object with all its associated lifecycle methods and event handlers, is to add the listeners for those lifecycle events:

utils:log("endingScene", "Beginning execution");

scene:addEventListener("createScene", scene);
scene:addEventListener("willEnterScene", scene);
scene:addEventListener("enterScene", scene);
scene:addEventListener("exitScene", scene);
scene:addEventListener("didExitScene", scene);
scene:addEventListener("destroyScene", scene);

return scene;

When that’s done, the code has only to return the scene object so it can be used by the Storyboard API.

As a famous pig once said: “That’s all, folks!”

Summary

In this chapter, you looked at the last bit of code that makes up Astro Rescue, the ending scene. In the process, you explored some new Corona functionality, including gradients and masking. You had a chance to see one last use of the über-useful Transition API as well as some old friends such as event handling and audio.

In the next chapter, you’ll look at some miscellaneous bits of Corona functionality that, while not used in Astro Rescue, most certainly could have been, and that you may well want to use in your own game projects. Some of those things include online scorekeeping, embedded database support and in-game ads (typical of “free” games), to name a few.

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

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