Corona SDK HOTSHOT

2.5 (2 reviews total)
By Nevin Flanagan
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Bat Swat – An Introduction to App Event Cycles

About this book

If you've used the Corona Software Development Kit to build your very first new mobile app, you already know how easy it makes developing across all the pieces of this fragmented market. This book upgrades your knowledge of Lua and the Corona API with designs, habits and advanced concepts to speed your development and create more exciting apps.

Corona SDK Hotshot will show you how to combine advanced Lua features such as coroutines and metatables with Corona's sophisticated tools, including physics and networking, to develop exactly the game or app you or your customers need, quickly and with an eye towards updating your app with improvements in the future.

Corona SDK Hotshot will expand your basic knowledge of Corona with an insight into making the most of its event platform, using physics wisely and easily, and moving on to advanced programming tasks like path-finding.

You will focus heavily on how to keep your programs understandable as they become more complicated, by using modules and events to divide it up. You'll practice ways to make AI scripts and map files easily understandable to designers and other collaborators, and use networks like GameCenter to publish progress.

The last projects will combine the full range of covered material to illustrate how you can produce sophisticated and exciting apps as a Corona Hotshot!

Publication date:
May 2013
Publisher
Packt
Pages
334
ISBN
9781849694308

 

Chapter 1. Bat Swat – An Introduction to App Event Cycles

The biggest challenge with any large project is managing the various elements that make it up and keeping them in sync with each other. Different systems have evolved to help make this easier (object-oriented programming being the most famous). Corona and Lua include features to support object-oriented thinking, but the most prominent feature of Corona that helps the app designer manage communication between program elements is a design pattern, which academics refer to as the publish-subscribe or observer-target model; Corona calls it events and listeners.

 

What do we build?


We'll start exploring this concept with a simple game, where creatures race across the screen, and you have to tap them before they escape off the other side. While the project design is simple, it provides an excellent arena to test and review the process of translating between low-level events provided by the system, and high-level events that describe events in the abstracted game.

This game project also illustrates how to construct different sections of an app using scenes in Corona's storyboard library and how to pass information between them. For instance, the gameplay scene forwards your final score to the menu screen to be considered for high-score status.

What does it do?

The app launches to a menu screen that displays the app name and an instruction to tap the screen. If left unattended, it also displays the high-scores list. Once the screen is tapped, it proceeds to a game screen depicting the ramparts of a tall castle, panning up over the walls as bats fly past. If you can tap the bats before they escape off the screen, they fall off the screen and you gain points. After a certain number, the game reckons up your score based on the number you managed to catch and submits that score to a list of recorded high scores to see if it has earned a place.

How is it excellent?

This system allows multiple objects to be notified when something happens in your program; the important part is that the point where that thing happens doesn't really need to know how many other parts of your code are interested, or what they'll do in response. It also allows a program to be separated into layers which consist of different objects. Each layer defines the events that outside objects might be interested in and adapts between low-level events (like collisions and touches on the screen) and higher-level abstract events (such as enemies dying or creatures coming within range of each other).

You'll be getting a feel for how event-based communications help manage multi-part projects, and you'll do it along with a refresher on how easily Corona lets you assemble a simple, but very playable, game. The logic as presented in the Lua language helps streamline a lot of chores that require many steps in languages like Objective-C.

The project also provides a good foundation for refreshing our acquaintance with some commonly used Corona facilities, notably the storyboard library, the transition library, and the sqlite3 database system. All of these will be important as we go forward. This project provides a low-pressure environment to catch up with them and refreshes your memory, which will serve you in good stead.

How are we going to do it?

While the development cycle is very simple, its structure forms the basis for planning basically any project. The structure is as follows:

  • Describing the game

  • Defining the event flow

  • Creating the game scene, and the bat and world objects

  • Creating the interface layer

  • Adding the shell

  • Tracking high scores

  • Adding some polish

What do I need to get started?

To complete the core of this project, you will need a small sprite to represent your elusive creatures, in the neighborhood of 32 x 32 pixels. To add the scrolling and parallax backgrounds for gameplay, a large wall graphic (about the size of your mobile device screen) and a distant background image (ideally taller than your target display) are also needed. Additional graphics make the menu screen more interesting.

A complete package of suitable graphics is included in the project download files at www.packtpub.com. These graphics, like the others in the book, were obtained from http://opengameart.org/ and are freely available for reuse under a Creative Commons By license. This allows you to include the graphics, free of charges or royalties, in any project (even commercial ones), as long as the project gives proper credit to the creators of the works used.

 

Describing the game


The first step to constructing any project, especially any software project, is to document the goals and requirements of the project. A software engineering professor of mine was fond of saying that "if you don't know where you're going, how can you know if you've gotten there or not?" Having a description in writing is a valuable tool for a single developer, because it gives them a way to track all their relevant thoughts and goals and remember what their initial intentions were. It is also an invaluable tool for teams of developers, because it gives them a central source for their task targets and a place to answer many of their questions without taking up time in meetings or in-person discussions for trivial answers.

Getting on with it

Since this project is focused on a couple of simple technical goals, the game design is simple and the description will be short and fairly informal. Create a new text document in an editor of your choice, and enter (or paste) each block of quoted text into it as we review them.

  1. A design should start with an overview description that summarizes what the player will experience and why it will be fun:

    Bat Swat is a game that tests your reflexes, challenging you to quickly tap sinister bats as they escape across the screen from the bottom left to the top right. Each bat knocked out of the sky is worth ten points, and the ten highest scores are displayed on the menu screen with the initials of their achievers.
  2. The design progresses to explain the appearance of the game and the gameplay in some more detail:

    The bats' flight is displayed across the staggered ramparts of a gothic castle, which move down and left across the screen to show upward progress. A distant landscape is slowly moved down in the background to reinforce the impression of upward travel. A numerical display of the player's score so far is shown in the upper left. A game lasts until 30 bats have appeared on the screen and either escaped off the other side or been knocked down.
  3. The design also explains what parts of the game are required in addition to the gameplay screen itself, to meet players' other expectations such as high score lists:

    Between games, and when the app first launches, a splash screen is displayed with the name of the game and a reminder to tap the screen in order to begin play. If the game remains on this screen for more than a few seconds, it cycles between a display of the high score record and the game credits.
  4. The design will explain the specifics of how high scores are selected and displayed:

    When the menu is displayed after completing a game, if that game's final score is within the ten highest scores recorded, a pop-up screen is shown displaying the new high score and soliciting the player's initials. This disappears and returns to the normal menu screen once the initials entry is confirmed.

What did we do?

Having this body of text handy gives us a touchstone and a definite target. Rather like the adage that you can sculpt by starting with a block a marble and chipping away everything that doesn't look like an elephant, our task as a developer is now to correct every point in the project that doesn't conform to our target description.

Committing our thoughts to writing is also important because it requires us to organize our design ideas and clarify ambiguities. Your design document serves as your preliminary rubber duck, a receptive listener to explain your ideas to in order to understand them better yourself.

What else do I need to know?

Many people get intimidated by the thought of writing design documents, concerned about being shackled into their original vision, or being unable to make changes as their ideas develop. It's important to remember that the design document is a component of your project, just like your code and graphics. Just as these components will be developed or replaced, expect your design document to evolve as your project develops. If you are coding and decide something needs to change, you can update your design document to reflect your new intentions.

If you are storing your projects in a version control repository such as git or SVN (which you should be for all but the most trivial projects), it's also an excellent idea to store your design document in that repository so that you can go back and check how the project goals have developed.

Other developers are so excited about their ideas that they view writing a design document as a useless obstacle to seeing those ideas come to life as quickly as possible. While it's important to focus your energies on parts of a project that produce visible rewards and keep you most engaged, the bottom line should be that any code you produce without at least an outline of a design document isn't your project; it's a prototype of your project and you should be prepared to replace any or all of it when your design is finalized. A project written directly from inspiration tends to be composed of pieces that have trouble interacting cleanly, and it will become increasingly disorganized as early testing changes the design.

 

Defining the event flow


To make sure that the game operates cleanly and the code is maintainable, we will first define the abstract events that take place at game level, and then explain the layer that translates lower-level events into those needed events.

Getting on with it

We need to consider what sorts of events take place at which levels, starting from the top, because the output from the highest-level processes is what ultimately interests us as developers:

  1. The game itself has two major events, its beginning and end. It also has one other important event, when the player's score changes; this event is used to keep the score counter in the corner of the screen updated.

  2. In order to determine when important game events occur, the game creates, intervenes in, and listens to a world. A world is a fairly self-contained system where actions whose results are relevant to the outcome of a game are resolved.

    Note

    For example, in a game of baseball, the outcome of the game is expressed in scores for the teams, determining a winner and a loser, but these are abstract concepts; the scores are determined by how the rules of the game are applied to things that happen on the field; for example, which hits are caught and which lead to runners on base.

    At the most basic level, the outcomes of these actions are determined by details like the laws of physics and players' reach and skill, whether a player can get into position to catch a ball flying in a certain direction, and so on. It is in the context of the world that we know a player has reached a particular base; it is in the context of the game that base is identified as home plate and that by reaching it, the player has accrued a run for their team.

  3. So, we need to determine what events in our world will be relevant to our game. The game score increases when creatures in the world are defeated by player action. The game is complete once a certain number of creatures have left the world, either by escaping off the borders or by being destroyed. So, the world needs at least two events; one indicating that a creature in the world has died, and one indicating that a creature in the world has despawned. It's worth noting that every death event will also be followed by a despawn event for the same creature, once the death animation has finished.

    The world generates events for deaths by listening itself to the various creatures spawned into it, waiting for them to post death events to themselves. It also waits for events to be posted for those creatures who are removing themselves from the world, and posts Despawn events to itself accordingly.

  4. Finally, the creatures themselves are responsible for posting their own death events. Since our game model is very simple, they do this whenever they detect touch events on themselves. Additionally, Corona does not currently post events to objects to inform them when they are removed from the display environment, so the creatures will need to generate those objects themselves when they are ready to leave the world environment.

Tip

Downloading the example code

You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

What did we do?

Now we have a verbal description of what layers are required to handle the flow needed to make the game work. In a more formal setting, this would be part of a program design document, which is not the same thing as a game design document; game design documents describe game rules, program design documents, and describe how software will be written that implements the game design (or other software; Corona is not just used for games).

Broadly speaking, we now have a plan for events and the layers of the program that they happen at; the plan looks something like the following image (arrows point from events to the objects that listen for them):

Another way to think of the event model for a piece of software is in terms of the event flow; how events occur on specific targets and trigger listeners that dispatch the same or related events to new targets:

What else do I need to know?

This design process, where we start with the most high-level, abstract part of the process and proceed further into the details until we reach whatever the underlying platform gives us, is often referred to as top-down design . Designing from the top down helps you build your code's foundation modules based on what the ultimate needs of your app will be, rather than restricting the final program to what you first thought you would need.

The biggest challenge when authoring in a top-down fashion is that you typically create software in such a way that it relies on components that haven't been written yet, so you can't run anything until you fill in the gaps. You can speed up the testing of top-level components by writing what are called stubs of the components being used, which are extremely simple functions that provide default responses.

 

Creating the objects


Now that there's an overall plan, it's time to start making that plan a reality and creating some actual code in a project we can run! As important as planning is, there's no denying that the exciting part of the project is watching the things you've been working on come to life and actually show up on the screen.

Getting ready

Using events in Corona requires tying your conceptual objects to specific types of Corona objects that support events. There are three types of such objects by default: display objects, storyboard scene objects, and the global Runtime object. (It's possible to create your own, but that's not something we're discussing at this point.) The creature objects are visible on screen and need to receive touch events, so it's logical to represent them with display objects. The world needs to hold a bunch of creatures and give them spatial relationships, so using a display group for it makes sense.

The game doesn't have a direct correspondence to anything on the screen, although it's responsible for showing the contents of the world as well as the display of the score. We'll make it a scene, because it has much the same life cycle and because we can use its associated display group to hold both the world and, later, the interface group.

Getting on with it

When you start the Corona Simulator, the splash screen asks whether you want to create a new project, launch the simulator, view your Corona Dashboard for info on apps you've released, or review demos and sample code.

  1. Click on the icon for New Project; the pop-up screen that you take next changes slightly depending whether you are running the simulator on Windows or Mac OS X:

    • On a Mac, enter the project name and select Scene from the template list. Leave the other two entries (size and default orientation) at their defaults and select the Next button. Choose a location for your new project file, then select Show in Finder from the last pop-up.

    • Under Windows, the dialog is somewhat more compact. Enter the project name and select Multiscreen Application. Use the Browse button near the upper-right corner to select whatever project directory you want to use. The OK button will open your project in both Windows Explorer and the Corona Simulator.

  2. Once a window is showing the project contents, find the file called scenetemplate.lua and make a copy of it. Name the copy game.lua.

  3. Open the file main.lua in the project folder using the text editor of your choice. Find the last line, where it says storyboard.gotoScene( "scenetemplate" ), and change it to storyboard.gotoScene( "game" ).

  4. Save this file. This changes the file which the project will look in for its initial screen content.

Loading art assets and libraries

Download the project pack, if you haven't already:

  • Copy the images directory and its contents into your project folder. This includes both the sprite sheet for our creatures and the background graphics for our world, as well as some images we'll use later to create our splash screen.

  • Also, copy the files world.lua and bat.lua from the version 1 subfolder into your directory. We'll include these files in the main game file and discuss their contents later.

Loading the world

Open the file game.lua. This file is prepopulated with the skeleton of a storyboard scene, which we'll fill in with code to run the game, and supplement with listener functions to respond to world events.

Find the block for the scene:createScene function and start replacing the comment that talks about inserting your own code with several new lines:

display.newRect(group, 0, 0, display.contentWidth, display.contentHeight):setFillColor(0, 0)

  self.World = require "world" {
    Backdrop = "images/exterior-parallaxBG1.png",
    Tile = "images/wall.png";
    Inhabitants = {
      bat = require "bat"
    }
  }
  group:insert(self.World)

This loads and calls the world.lua module, which generates a new function that constructs a world object with the specified options.

Note

Depending on how much Lua programming you've done, the syntax of the require call may or may not be familiar to you; when a name or expression is followed directly by a string literal or table constructor, Lua attempts to call the value of the name or expression (assuming it is a function) with the string or table as its only argument. So the name require followed by the literal world is equivalent to the function call require "world". Since require returns whatever is returned by the module it loads, and world.lua returns a function (more on this in a bit), the require "world" call itself ends up being equivalent to a function, and followed by the table constructor, the {braces}, and their contents, it becomes a call to that function using that table. The table specifies graphics files to be used by the new world object for its presentation, as well as a list of creatures that the world needs to be able to include and create.

Then, we take the newly created world object and add it to the scene's display group so that it will appear on the screen and be hidden or deleted properly when our game leaves or purges the scene.

Linking the game with the world

Finally, we establish some event trigger relationships between the game object and the world object that it has created:

  group:insert(self.World)

  self.World:addEventListener('Death', self)
  self.World:addEventListener('Despawn', self)

  self:addEventListener('Game', self.World)
end

As specified previously, the game object will listen to the world for events where a creature has been defeated or has otherwise despawned, so that it can determine when the player scores and when the game should end. It also registers the world object to receive game-related events such as the game beginning and ending. Note that the game has no idea how the world object will respond to these events, or even if it will respond at all. It simply makes sure that the world will be notified when these things happen.

Loading a new game into the display

The createScene event is dispatched to scenes only when they are first loaded or if their displays have been unloaded to save memory. When the scene actually begins or is reloaded from a different scene, the createScene event might be skipped, but the enterScene event always fires.

  1. We'll go down to the next event responder, and replace the contents of that function with code that actually starts the game:

    function scene:enterScene( event )
      self.ScoreTotal = 0
    
  2. When a game starts, the player's score should always be reset to 0:

      self.ScoreTotal = 0
      self.StartingCount = 1
      self.Count = self.StartingCount
    
  3. While the design calls for 30 creatures traversing the screen in turn, first we want to make sure that the basic mechanisms we're creating work. So to start, we will create only one bat so that we don't have to wait for the whole scene to finish in order to try again. While the game only needs to track the remaining count of creatures, we remember the total count we were going to spawn for the game, so it can be stored with high-score information easily:

      self.Count = self.StartingCount
      self:dispatchEvent{name = 'Game'; action = 'start', duration = (self.Count + 1) * 1500}
    

    This is our first custom event that we trigger! This notifies anyone who's listening (like, possibly, the world) that a game is about to begin, and that it's expected to last a certain number of milliseconds. Many games will not have a fixed duration field, but we want to provide an estimate of the game time so that the scrolling parallax background will move continuously throughout the game. This code allows a second and a half for each bat (since that's how often they'll be released), plus another second and a half for the last bat to cross the screen.

    Note

    Notice that the event's name starts with a capital letter. All Corona built-in events have names beginning with lowercase letters; using uppercase letters to start the names of our own custom events ensures that we won't have any naming conflicts with Corona, and also makes it easy to recognize basic events from our own.

Preparing the game challenges

Once we've triggered the game start, we prepare the bats to be dispatched:

  self:dispatchEvent{name = 'Game'; action = 'start', duration = (self.Count + 1) * 1500}
  for i=1,self.Count do
    local x, y = 0, display.contentHeight,
    timer.performWithDelay(i * 1500, 
      function(...) 
        self.World:Spawn{kind = 'bat', x = x, y = y, 'brown'} 
      end
    )
  end
end

This loop prepares the creation of as many bats as the game level is supposed to release. Right now, it will generate only one, because the StartingCount event has been reduced to 1 for test purposes; but increasing that number will automatically schedule more bats every second and a half.

The timer.performWithDelay pattern is probably a familiar one; it creates a new temporary function on each pass through the loop, which will call another function with the parameters specified in that pass through the loop. The x and y values specified are currently fixed for testing purposes, but later we will add some variety so that the bats do not all follow the exact same line.

Responding to world changes

Finally, we go up to the top of the scene file after the storyboard.createScene() call, and define one more function of the scene object. The scene has been registered as a listener for Despawn events on the world, so we need to explain how the game should handle those events when they occur:

local scene = storyboard.newScene()

function scene:Despawn(event)
  if not tonumber(self.Count) or self.Count <= 1 then

Note

When a table (which includes scene and display objects) is registered as a listener on some event target, it should have a field name matching the event name (including matching case; Lua is case-sensitive), containing a function that will process those events. If it does not contain such a field when the relevant event occurs, it will simply be ignored until the next such event (later, we will use this deliberately). The game is concerned about these events because once all the bats it directed the world to spawn have despawned, the game is complete.

Monitoring game progress

The first thing we do when a creature despawn occurs in the world is check how many more despawns we're expecting. As a sanity check to avoid errors, if the count of remaining creatures is missing or not a number, we also assume that the game is over:

  if not tonumber(self.Count) or self.Count <= 1 then
    self:dispatchEvent{name = 'Game'; action = 'stop'}

Concluding the game

If the count of creatures still waiting to spawn isn't more than one, indicating that there are extra bats still waiting their turn, then the game is over (this count reduces by 1 for each bat that spawns). We dispatch a Game event with the stop action to notify interested listeners (like the world) that the game is over:

    self:dispatchEvent{name = 'Game'; action = 'stop'}
    os.exit( )

Once other game elements have performed end-of-game cleanup, we leave the game. Eventually, we will want to return to the menu screen and submit our score to the high-scores table, but that module doesn't exist yet:

    os.exit( )
  else
    self.Count = self.Count - 1
  end
end

If the game is still waiting for more than one creature to pass, all we need to do is reduce the count we're waiting for by one to account for the count that just passed.

The game module is now ready to test! Before we finish, we can just go down towards the bottom of the file, and remove the skeleton functions for exitScene and destroyScene, as well as the scene:addEventListener calls for them. They will not be needed for this module in this project.

Understanding your libraries

It's worth examining the world.lua and bat.lua modules briefly, because they implement the rest of the critical event chain that we designed in the second step. Notice in world.lua how the world constructor function adds a function to the world object called self:Game(event). Since the game registered the world as a listener for Game events by itself, this function will be called automatically when the game object sends a start or stop event.

The world object also provides the Spawn function that the game calls to create new bats, and one of the things it does there is add itself as a listener for Death and remove events on the newly spawned creature. It responds to Death events by reposting them to itself, and to remove events by posting Despawn events for the game to pick up.

The bat.lua module has a couple of points of interest. We've covered in the abstract how custom events can be substituted for physical events; the bat illustrates this directly, by publishing a Death event to itself when it is tapped. It then also unregisters itself for future tap events so that you can't repeatedly tap dead bats for extra score.

Note

Notice that the bat object registers for its own Death event, as well as the world registering for it. It uses this so that it can separate the cosmetic reaction to its death (currently very simple) from the logical events required to make it happen. It also means that it will respond properly even if something else posts a Death event to it from outside.

What did we do?

At this point we have a rudimentary test game that illustrates the flow of events up and down the object layers. For the sake of brevity, we imported existing modules to support two of the three layers we need to deal with, and constructed the third one to control and respond to the middle layer. The game layer does not ever interact with the individual bat objects, honoring the programming principle called encapsulation or weak coupling .

So far, the game will run a single bat across the screen and then quit. If you manage to tap the bat, it will fade out but finish its trip across the screen. Obviously, this is not a very satisfactory game for the work needed to produce all these files (assuming that you had to write all three of them yourself). However, the next few tasks will show us how comparatively easy it is to incorporate these features into the robust infrastructure we have established.

 

Creating the interface


While many game designers consider games that require no visible interface to be the platonic ideal of their craft, nearly every game requires some sort of extra-diegetic interface element, something that provides information about the game world and accepts commands into the game world, but is not itself part of the game world. To show the player how well they're doing, we'll add a layer above the game world, with a number showing the player's current score, and increment it whenever their score changes.

Getting on with it

First, we're going to prepare the interface module. This code will live in a separate file to make it easy to maintain without changing the game code, and vice versa, once the initial connection is made. The interface module will be a function that takes a game object and returns the group containing the various interface elements. Start by creating a new text file in the project source directory named interface.lua and framing in the outline of the function:

return function(game)
  local self = display.newGroup()

  return self
end

This gives us the groundwork for a function that returns a new group. Now we can start filling in the body with elements for the group (or one element, in this case).

Adding visible information

First we create the text object that will display the score:

  local self = display.newGroup()
  self.ScoreDisplay = display.newText(self, "000", 20, 10, native.systemFont, 24)
  self.ScoreDisplay:setReferencePoint(display.CenterRightDisplayPoint)

Setting the reference point doesn't actually move anything on the screen, but it does change which point in the text is considered by Corona to be the x and y coordinates of the object. This will make it easier to keep the text aligned as we update it later.

Updating an information display

We want the score display to change when things happen in the game, so it will need to listen for the relevant events:

  self.ScoreDisplay:setReferencePoint(display.CenterRightDisplayPoint)
  function self.ScoreDisplay:Score(event)

Because the score display object will be used as a listener that responds whenever the game's score changes, it will need a function field that responds to Score events.

  function self.ScoreDisplay:Score(event)
    local x, y = self.x, self.y

Because Corona text objects are not naturally aligned, some juggling is needed whenever one might change its text contents and therefore its size. The x and y values we record here are the ones where we always want the center of the text's right edge to appear.

    local x, y = self.x, self.y
    self.text = string.format("%03d", event.total)

The format call ensures that the displayed score is always three digits long, with leading zeroes as needed. This keeps the score looking consistent.

    self.text = string.format("%03d", event.total)
    self:setReferencePoint(display.CenterRightDisplayPoint)
    self.x, self.y = x, y
  end

This ensures that we will be placing the text at its new center-right anchor. Also, it sets that point back to the originally recorded coordinates. Now that we've concluded the function to respond to score changes, we need to register that we're interested in hearing about the scores, before we return the new interface to the game creating it:

  end
  game:addEventListener('Score', self.ScoreDisplay)
  return self
end

Linking the interface to the game

Save the file and close it. We now have a functional interface layer, but it isn't yet being created or used. Open the game.lua file and locate the scene:createScene function block. After the lines that create and insert the world group, add similar lines for the interface module:

  group:insert(self.World)
  self.Interface = require "interface" (self)
  group:insert(self.Interface)

  self.World:addEventListener('Death', self)

We load the interface creator function, and call it, passing it a reference to the game it will listen to for Score events. We then insert the interface into the game scene's display group at a higher layer than the world layer, so that it rides on top (otherwise, the world background would hide the interface).

Triggering a game event from a world event

However, although the interface layer is now being created and displayed, and it's listening for Score events on the game, these events are not being published yet. Near the top of the open game.lua file, above the scene:Despawn function, add a function, similar in form to the despawn function, to handle death events and modify the game's score accordingly.

local scene = storyboard.newScene()

function scene:Death(event)

The first thing we will need to do when we detect that a creature has died in the world (rather than just leaving the world borders and despawning) is increment the game score:

function scene:Death(event)
  self.ScoreTotal = self.ScoreTotal + 10

Then, after changing the actual score, we need to broadcast that the score has changed. This will finish the Death response handler:

  self.ScoreTotal = self.ScoreTotal + 10
  self:dispatchEvent{name = 'Score'; total = self.ScoreTotal}
end

function scene:Despawn(event)

Tip

Lua is a language that's light on semi-colons, making their use between statements optional in almost all cases. In this case, table constructors allow items being added to the new table to be separated with either commas (most of the time) or semi-colons (only occasionally). I like to use them to separate table elements into groups; in this case, I use one to separate the event name (which all events have) from the other parameters (which are particular to each specific event).

Now, the score should increment as you successfully tap the bat flying over (if you want to test it more thoroughly, try changing the game scene's StartingCount field from 1 to 5). The last precaution we need to take is to reset the score properly if the scene is reused for a new game without being unloaded first, by adding an event inside the scene's enterScene response:

function scene:enterScene( event )
  self.ScoreTotal = 0
  self:dispatchEvent{name = 'Score'; total = self.ScoreTotal}
  self.StartingCount = 5

What did we do?

We created a display to show the game score, and set it to update automatically as the game score increases. We made the display aware of the specific game whose score it will display, by passing that game to the interface constructor. We also modified the game to actually produce these events, so that the interface will have some updates to process.

What else do I need to know?

The world object doesn't register itself with the game object for events; it lets the game object do that for it and only provides the response. Why do we instead give the game object to the interface layer and let it register itself?

The main reason is that the world is primarily a source of events for the game to listen to, whereas the interface is primarily interested in events that the game object generates. In a more complex game, it would be very difficult for the game object to anticipate every possible event that the interface might need to display, and every new event the interface wanted to handle would require modifying the game module as well to register it. This approach lets the interface call the shots as far as registration, whereas the game can reasonably assume that it will drive most communication that goes from the game into the world.

 

Adding the Shell


Most arcade-style games like this one have one or two screens to fill the gap between games, provide instructions, and display score records. Corona's storyboard module makes it easy to implement this splash screen as a separate scene and pass useful cues between the two.

Getting ready

Copy the file visuals.lua, from the version 3 subfolder in the project pack, into your project directory. This is a library that provides some visual effects functions; each one takes the object to perform the effect on, and returns a function that can be called to stop the effect.

Getting on with it

Because most of the code in the menu module is just the straightforward loading and positioning of images, we'll copy the module into the project directory rather than code the entire thing from scratch. Copy the file menu.lua, from the version 3 subfolder in the project pack, into your project directory and open it in a text editor so we can review the points which are more interesting:

local scene = storyboard.newScene()

function scene:tap(event)
  storyboard.gotoScene("game")
  return true
end

local function alternate(object)

This sets the scene up to be used as a listener for tap events. When the registered target is tapped, the scene will initiate a change to the game scene, which will trigger the initialization of a new game. In order for this to do anything, the scene object must be tagged as a listener (the scene's view group will receive touch events of many of its children that don't handle their own events):

function scene:createScene( event )
  local group = self.view

  group:addEventListener('tap', self)

  self.Backdrop = display.newImage(group, "images/splash.png", 0, 0)

Creating a staging zone for high scores

We'll add a group on the screen where high scores can be displayed:

	scene.ScoresWindow = display.newGroup()
	scene.view:insert(scene.ScoresWindow)
	scene.ScoresWindow.x, scene.ScoresWindow.y = 40, 190

This display group, presently left empty, is positioned in the large blank space in the lower-left corner of the splash screen. In later updates it will give us a suitable place to display the high scores list and game credits.

Linking the shell into the play cycle

In order to actually use this scene, we need to adjust the main.lua file or it will just continue opening the game scene. Open main.lua and change the last line from storyboard.gotoScene( "game" ) to storyboard.gotoScene( "menu" ). This will change the first scene loaded when the game starts.

Finally, now that we have somewhere to return to, we don't have to quit the app when the game is over. You can open game.lua, find the line in the scene:Despawn function that reads os.exit( ), and replace it with the following:

    self:dispatchEvent{name = 'Game'; action = 'stop'}
    storyboard.gotoScene("menu")
  else

Now the game will loop back to the menu screen after the game is done!

What did we do?

Now we have the basic structure typical of arcade-style games; launch to splash screen, proceed to game, return to splash screen. We not only incorporated a new scene module into the project, but we made the connections to set it as our initial scene and revert back to it after each game is over.

What else do we need to know?

When a display object is touched or tapped on the screen, the object itself is the first one to be informed of the event. However, if it does not mark the event as handled (by returning a truthy Lua value, one which is not nil or false, from the event handler), the event will continue to be passed up to its ancestor groups, in reverse order, then to other objects positioned behind it and their ancestors, until finally it is dispatched to the global object Runtime or until one of the objects declares it taken care of. Therefore, a tap on any object which is part of the menu scene will eventually be passed up to the scene's display group, which is the ancestor of all of them.

Note

In scripting languages, truthy values are ones that are considered on or positive for if statements. In Lua, all values are truthy except for nil and the Boolean value false; even the empty string and the number 0, which are treated as false in many other languages, are considered truthy in Lua.

 

Tracking high scores


The last ingredient to meet our requirements is high-score tracking. We'll need to pass the final scores from the finished game, collect initials (in true arcade fashion) for new high scores, and maintain the database of saved scores.

Getting ready

Copy the enterInitials.lua scene file from the version 4 subfolder of the project pack into your project directory. This scene is fairly straightforward and adds the pop-up screen that will collect players' initials when they reach a new high score.

This section uses the sqlite3 library included with Corona to simplify managing score records. While we'll spend some time discussing the intent of the SQL statements included, a detailed discussion of SQL syntax is thoroughly outside the scope of this book. For a good, basic introduction, visit http://www.w3schools.com/sql/.

Getting on with it

We're going to start by creating a wrapper module to save the rest of our program from dealing directly with the database. There are three basic tasks that the rest of the program needs the database to do:

  • Retrieving a top score

  • Determining if a score belongs in the top 10

  • Adding a new score to the database

Linking to the database file

Create a new text file in the project directory called history.lua and open it. Start by loading the needed library:

local sqlite3 = require "sqlite3"

This loads Corona support for working with databases and establishes the local name sqlite3 as your interface for using it. Now it's time to open and prepare the database as needed:

local sqlite = require "sqlite3"

local storagePath = system.pathForFile('BatSwat.history', system.DocumentsDirectory)

local db = sqlite3.open(storagePath)

Because the Resource directory is read-only on mobile platforms, we store the score database in the Documents directory. The sqlite3.open call attempts to load the given file location as a new database, and creates a blank database file if no file was found:

local db = sqlite3.open(storagePath)
Runtime:addEventListener('system',
  function(event)
        if( event.type == "applicationExit" ) then              
            db:close()
        end		
  end
)

We'll get more into dealing with closed and paused applications in the next chapter, but this just makes sure that the database will be flushed and closed properly if the application is closing.

Initializing the database

We'll need a list in the database where we can store score values:

  end
)

db:exec[[
  CREATE TABLE IF NOT EXISTS history (
    happened PRIMARY KEY, 
    Score, 
    Count,
    Initials
  );
]]

This is our first taste of SQL, where we have the database run with the :exec method. It just makes sure that the database has a table to hold our high scores, including when the score was achieved, how much it was, how many creatures there were on the whole level, and who got the score in question. The time the score that was achieved is used as the table's primary key, meaning there can be only one score for any given moment of completion. As the game is single-player, this is fine. If the database already existed, this line will do nothing.

Note

Notice the use here of the Lua long string literal, enclosed in double brackets. This form ignores escape characters, quotes, new lines, and everything else except its closing bracket, simply treating them as characters in the string. This makes it ideal for incorporating code from other languages, reducing the likelihood that some element from the stored code will end the string prematurely or be misinterpreted by the Lua parser.

Cleaning up old scores

We don't need to keep more scores than we can display.

  );
]]

db:exec[[
  DELETE FROM history WHERE happened NOT IN (SELECT TOP 10 happened FROM history ORDER BY Score DESC)
]]

This is a maintenance line. We don't want the list of high scores to just keep getting bigger and bigger and taking up more of the user's device memory, so whenever we launch the game, we clean out any scores that have fallen off the bottom of the list. The SQL statement here basically says, "make a list of all the times that have scores in the top 10, and then delete every score whose time isn't on that list."

]]

local history = {}

return history

Here, we're just preparing the history module that will be returned when the file is loaded. The three functions that are the substance of the module will be added between these two new lines, since the return must go last.

Note

While programming styles vary, it's frequently a good habit to build your code inwardly; for instance, when you type the beginning of an if … then statement in Lua, you can immediately type end on the next line and then back up and fill the contents in between the two. This approach helps you avoid forgetting to close function calls, long strings, loops, and other things that can end up unbalanced.

Considering possible new high scores

To see if a new score qualifies as a high score, we'll see where it falls among the scores already gathered:

local history = {}

function history:find(score)
  for count in db:urows([[ SELECT count( ) FROM history WHERE Score >= ]] .. score) do
    return count + 1
  end
end

This function identifies where a proposed score would fit into the list. We'll use it to identify which new scores have a place in the top 10 and should ask for the player's initials. It works by counting the number of existing score entries that are larger than the score under consideration.

The sqlite3 library offers three different functions for scanning through the results of a SELECT query. The db:nrows method returns a table representing the row, with named fields matching the column names that hold their values for that record. The db:rows function gives back a table which simply holds the value of the first column at index 1, the value of the second column at index 2, and so forth. The db:urows function, used here, simply uses Lua's multiple returns to pass back all the values from the record without making a new table, in the same order they would appear in the table returned by db:rows.

Tip

It's worth noting that all three functions don't actually return any records; they return iterator functions that produce the contents of a new row each time they're called. This makes them ideal for use in Lua generalized for loops.

Saving new high scores

When a new score is identified as being better than a previous high score, we need to record it:

end

function history:add(statistics)
  if statistics.HighScore <= 0 then return end

This function will submit a new score to the database. If for some reason a score of 0 was submitted, we won't bother storing it:

  if statistics.HighScore <= 0 then return end
  db:exec(
    string.format([[
      INSERT INTO history VALUES (
        datetime('now', 'localtime'),
        %d, %d, %q
      );  ]],
      statistics.HighScore,statistics.MobCount, statistics.Initials
    )
  )

Here, we use the string.format function (a close relative and derivative of the C printf) to fill in the specific information provided to us about the score into an otherwise preprogrammed SQL INSERT command. Executing the finished command adds the new row into the database.

Recovering old high scores

To display the scores, we'll need to retrieve them from the database:

  )
end


function history:TopScore(index)

The last function we add will retrieve the score with a given index; 1 for the highest score, 2 for the second highest, and so on:

function history:TopScore(index)
  local query = [[SELECT * FROM history ORDER BY Score DESC LIMIT 1 OFFSET ]] .. (index - 1)

Here we prepare the query. The LIMIT 1 clause means we only want one value from the list, and the OFFSET clause indicates how far down the sorted list we want to find that value, basically like an index into an array:

  local query = [[SELECT * FROM history ORDER BY Score DESC LIMIT 1 OFFSET ]] .. (index - 1)
  for info in db:nrows(query) do

To make it easier for the code calling this function to use the result, we use the db:nrows iterator to get back a table structured like a record, with named fields for the column values.

  for info in db:nrows(query) do
    return info
  end

Like most database functions, the luasqlite3 iterators aren't really intended to be used with single values. We could save the obtained record in a variable local to the function and trust the loop to exit after one pass (since the query statement specified should never return more than one record), but just returning out of the loop on the first pass also works fine, since we have nothing else to do after finding the first record.

Since the function will always return from the first pass through the loop, there's no need for any other body, and we're done with the module:

    return info
  end
end

return history

Communicating scores between modules

Now that the score tracker is ready, we need to prepare the other modules to use it. First, we make a small change to the game scene file, to make it pass its final score back to the menu scene for consideration. The storyboard library has added the ability to hand parameters off to a scene when you load it which is perfect for this purpose:

function scene:Despawn(event)
  if not tonumber(self.Count) or self.Count <= 1 then
    self:dispatchEvent{name = 'Game'; action = 'stop'}
    storyboard.gotoScene("menu", {params = {Score = self.ScoreTotal, Count = self.StartingCount}})
  else
    self.Count = self.Count - 1
  end
end

This way, the menu's enterScene function will be able to access the score and count as fields of the event.params table. This is the only change we need to make to the game.lua scene file. Next, open the menu.lua file to add support for receiving this data. Start by loading the history module at the top of the file so that we will be able to check whether the received score is a new record:

local history = require "history"

local storyboard = require( "storyboard" )

Now, we need to add support for using that module to check for a new high score and record the initials that qualify. If we have to pop up a collection window, we want to hold off on running any animations until we've returned from that process, so replace the unconditional call to the scene:Cycle()function, which handles showing the high scores and credits, with a conditional statement:

function scene:enterScene( event )
  self.Banner.alpha = 0

  if event.params and event.params.Score then
    if history:find(event.params.Score) <= 10 then
      storyboard.showOverlay("enterInitials", {effect = "fromBottom", params = {Score = event.params.Score, Count = event.params.Count}, isModal = true})
    end
  else
    self:Cycle()
  end

end

Reviewing new scores

First, we check whether we've received a score at all. Remember that this scene is also launched when the app starts up, in which case there will be no new score to forward.

Next, it uses the history module to ask whether the newly received score belongs in the top 10. If not, it won't be showing the initials entry screen and can go directly to running animations.

If this is a new high score, however, we need to display the enterInitials pop-up scene to collect user input. We use the storyboard library's showOverlay function to display the new scene over the current one, since we will be coming straight back to the splash screen when we are done. We pass scene and count to this function, just as we received them, so that the data entry screen can record them in the database. The isModal argument field prevents touches in the pop-up scene from drifting down into the menu screen while it is active.

Finally, we register the menu scene to notice when the score is recorded and the overlay is closed, so that it can start its animations. First, we specify that the Cycle function (which runs those animations) should be the scene's response to any overlays ending; then we make sure the scene knows that it is interested in its own overlayEnded events:

end

scene.overlayEnded = scene.Cycle
scene:addEventListener( "overlayEnded", scene)

function scene:exitScene( event )

Displaying the score history

Now that score processing is ready, we're going to add code to actually display the high scores. For the moment, we'll just lay them out in the designated space as soon as animations are visible. So, we'll add that call to the menu's scene:Cycle function:

function scene:Cycle()
  self.StopPulsing = visuals.PulseInOut(self.Banner)
  self.StopEffects = revealScores(self)
end

Because we're expecting this to be animated later, we're leaving open the option to have a transition that we might need to stop or change. Right now, we'll focus on just making the scores show up in the new revealScores function:

local function revealScores(scene)
  display.remove(scene.ScoresSlide)
  scene.ScoresSlide = display.newGroup()
  scene.ScoresWindow:insert(scene.ScoresSlide)
end

function scene:Cycle()

This adds a new group to store all our high score displays in, making it easy to animate or clear all of them at once. Before that, however, we remove any previous high-score displays to make room, since the high scores may have changed since they were last displayed:

  scene.ScoresWindow:insert(scene.ScoresSlide)
  for i = 1, 10 do
    local score = history:TopScore(i)

Next, we loop through the 10 highest scores in the history of the game. The score variable will actually be a table containing all the relevant fields:

    local score = history:TopScore(i)
    if not (score and score.Initials and score.Score) then break; end

If the game is new, the high score table might be mostly empty, so if we run out of scores, we finish the loop early:

    if not (score and score.Initials and score.Score) then break; end
    display.newText(scene.ScoresSlide, score.Initials, 0, 24 * (i - 1), native.systemFont, 16)
    local score = display.newText(scene.ScoresSlide, score.Score, 0, 24 * (i - 1), native.systemFont, 16)

We create the two text objects to hold the initials and the actual score. Creating two objects means that they can be aligned separately:

    local score = display.newText(scene.ScoresSlide, score.Score, 0, 24 * (i - 1), native.systemFont, 16)
    score:setReferencePoint(display.CenterRightReferencePoint)
    score.x = 100
  end
end

Finally, we align the score number on the right-hand side of the available space. The score reveal is now basically complete!

What did we do?

By recording high scores and allowing people to compete for the best, we've finished adding the core criteria required by the design document. Some features that were described aren't implemented yet, but they're all fairly cosmetic in nature. That means the game is functionally finished, and now is a good time to test it out. Play it repeatedly and look for anything that seems broken. Or, just keep playing it for a while; you've earned it!

 

Adding finishing touches


Although the game is functionally working at this point, it's not really ready for the prime time. The way the bats just fade out and keep flying could easily confuse players. The high scores table could use a little more excitement. Finally, since we're reusing Creative Commons art assets, some credit information is in order.

Getting ready

Copy the explosion.lua file from the version 5 subfolder of the project pack into your project directory. This file provides the sprite sheet info and a simple function to create and animate a small explosion at a given point.

Getting on with it

First, we're going to make the bat's death a little more dramatic, adding a little explosion and causing the bat to fall off the bottom of the screen instead of continuing to fly. Open up the bat.lua file and find the mobDied local function, which is registered to go off in response to the creature's Death event, and delete the line that says self.alpha = 0.3:

local function mobDied(self, event)
  transition.cancel(self.Flight)

The first thing we're going to do is stop the bat's normal flight course:

  transition.cancel(self.Flight)
  explosion(self.parent, self.x, self.y)

We'll use the new explosion module to create an animated explosion in the bat's world environment at its current point. The explosion is responsible for animating itself and deleting itself when the animation is done.

Changing the creatures' motion

We want the bat to seem to fall when it is killed:

  explosion(self.parent, self.x, self.y)
  transition.to(self, {time = 1000, x = display.contentWidth})

We'll start the bat sliding sideways off the right-hand side. Because we're looking for a natural bouncing motion under the effects of gravity, the horizontal aspect of the motion will be tweened linearly, while the vertical component will be tweened quadratically; this means we need two separate transitions:

  transition.to(self, {time = 1000, x = display.contentWidth})
  local distance = 100 + (display.contentHeight - self.y)

We calculate the total distance the bat will travel vertically to rise up 50 pixels and then fall off the bottom of the screen. We add 100 because it will also fall the extra 50 pixels that it bounced up:

  local distance = 100 + (display.contentHeight - self.y)
  transition.to(self, 
    {
      time = 1005, transition = arc, onComplete = clean;
      y = display.contentHeight
    }
  )

Animating on a custom curve

Because the bat will first bounce up a little, then fall, its vertical motion will be determined by a custom tweening function, arc, which we will write in a moment. This transition is made slightly longer than the first one, because it will clean up the object (and post a remove event) when it's done, and we want to make sure the other transition has finished first:

local function arc(t, tMax, start, delta)
  if t <= 250 then
    return easing.outQuad(t, 250, start, -50)
  else
    return easing.inQuad(t - 250, tMax - 250, start - 50, delta + 50)
  end
end

local function mobDied(self, event)

This is our custom tweening function. It isn't terribly fancy; it transitions the object back, against the direction of the tween, for the first 250 milliseconds, then tweens it forward from that point for the rest. It relies on Corona's existing quadratic tweens (a decelerating tween for the upward motion, and an accelerating one for the fall) to calculate the intervening values.

Adding visual interest to the high scores

Now the bat dies a little more dramatically and we can move on to animating the credits and high scores. Close bat.lua and open menu.lua, and add a new text object to the scene:createScene function:

  self.ScoresWindow.x, self.ScoresWindow.y = 40, 190

  local creditsText = [[
Bat Swat code:
  Nevin Flanagan
Images:
  Bat Sprite:
    MoikMellah
  Environment art:
    Jetrel
  Licensed from
  opengameart.org
  under CC-By 2.0]]
  self.Credits = display.newText(self.ScoresWindow, creditsText, -16, 0, 190, 144, native.systemFont, 12)
end

This credits object will trade places periodically with the high-scores display, so whenever it fades out, it needs to cue the high-score object to slide into the empty space:

  self.Credits = display.newText(self.ScoresWindow, creditsText, -16, 0, 190, 144, native.systemFont, 12)
  self.Credits:addEventListener('Faded', function() self.StopEffects = visuals.SlideInFadeOut(self.ScoresSlide) end)
end

We don't want the credits to appear until the high scores have faded out, so make them completely transparent whenever the scene starts:

function scene:enterScene( event )
  self.Banner.alpha = 0
  self.Credits.alpha = 0

  if event.params and event.params.Score then

This means that in order for it to ever appear, it needs to be cued in whenever the high scores fade out, which we'll set up in the revealScores function:

    score.x = 100
  end
  scene.ScoresSlide:addEventListener('Faded', function (...) scene.StopEffects = visuals.SlideInFadeOut(scene.Credits) end)
end

In order for the object to ever receive a Faded event, we'll have to start the same transition on it:

  scene.ScoresSlide:addEventListener('Faded', function (...) scene.StopEffects = visuals.SlideInFadeOut(scene.Credits) end)
  return visuals.SlideInFadeOut(scene.ScoresSlide)
end

This moves the collected scores down off the screen, and schedules them to slide back in after a second and a half. Returning the resulting cancellation function means that the Cycle function will store it in the scene.StopEffects field. This is important, because if we start a game, we need to be able to cancel that transition. We can do that from the scene:exitScene function.

function scene:exitScene( event )
  self.StopPulsing()
  if self.StopEffects then
    self.StopEffects()
  end
end

Parameterizing the game length

Finally, we need to update the number of bats per scene; right now, it is set to a test value of 5. The document specifies 30 per game. To leave ourselves room for flexibility in the future, we'll have the menu pass in the desired number when it calls the game scene, much the same way the game passes the final score out. Go to the top of menu.lua and add the following line:

local scene = storyboard.newScene()

local options = {params = {Count = 30}}

function scene:tap(event)

We'll reuse this table as we relaunch the game scene. Now, we just need to supply it when we launch the game, which is in the function right under that:

local options = {params = {Count = 30}}

function scene:tap(event)
  storyboard.gotoScene("game", options)
  return true

Adding a reference to the options table tells storyboard to pass the parameters in to the scene being opened. Finally, we can save and close menu.lua, open game.lua, and make one change to the scene:enterScene function:

function scene:enterScene( event )
  self.ScoreTotal = 0
  self:dispatchEvent{name = 'Score'; total = self.ScoreTotal}
  self.StartingCount = event.params.Count
  self.Count = self.StartingCount

Instead of setting the game's starting creature count to a fixed 5, we'll set it to whatever is passed in by the menu.

What did we do?

Now, we added additional animations to the dying bats by modifying only one function (and adding another one for it to call). We also added a little animation to create visual interest and made control of the game scene more flexible.

 

Game over – wrapping it up


We have engineered a game from concept description up through final polish and improvements. We have designed and implemented a flexible system for bridging communications between disparate elements of the program and adapted the components within that system to accommodate new elements as they are included. We also now have a playable game! How many bats can you swat?

 

Can you take the HEAT? The Hotshot Challenge


There are several generalizations that have been left in the code to make it easier to add new components and variations later with a minimum of fuss. Can you give the player a choice between the existing level and one that mixes the brown bats with black ones that move slower, but take three taps to kill? For the cleanest results, try and do it without creating any new scene files!

About the Author

  • Nevin Flanagan

    Nevin Flanagan has had an extremely varied career covering several fields, but the threads of computers, teaching, and games have trailed through it all for years. He has programmed on different levels ranging from assembly language to high-level scripting in game engines, and is credited as a contributor to the World of Warcraft user interface. He is currently fascinated by the interface possibilities offered by mobile touchscreen devices and is completing a Master's degree in Interactive Media and Game Development at Worcester Polytechnic Institute in Massachusetts. He lives with his wife Jenna in Leominster, Massachusetts, in the U.S.A.

    Browse publications by this author

Latest Reviews

(2 reviews total)
The book is way out of date and the code in the book will not run. There have been significant changes to Corona SDK since this book was written. Storyboard has been replaced by Composer and the info to migrate from one to the other is no longer available from the Corona website.
Good
Corona SDK HOTSHOT
Unlock this book and the full library for FREE
Start free trial