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.
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.
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.
scenetemplate.lua
and make a copy of it. Name the copy game.lua
.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" )
.Download the project pack, if you haven't already:
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.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.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.
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.
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.
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.
function scene:enterScene( event )
self.ScoreTotal = 0
self.ScoreTotal = 0 self.StartingCount = 1 self.Count = self.StartingCount
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.
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.
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.
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
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.
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'}
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.
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.
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.
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.
18.118.45.162