Atmosfall – Managing Game Progress with Coroutines

Exclusive offer: get 50% off this eBook here
Corona SDK Hotshot

Corona SDK Hotshot — Save 50%

A detailed guide with 10 projects specifically designed to expand the fundamentals of this exciting mobile development platform! with this book and ebook

$29.99    $15.00
by Nevin Flanagan | June 2013 | Games

In this article by Nevin Flanagan, author of Corona SDK Hotshot, we look at managing information with the use of physics engine. If you're a long-time or low-level programmer, you may be accustomed to controlling the core of your game loop; if you're more used to recent game engines, you may have learned to juggle complex information about what actors in your game are doing at any given moment. In Corona, the combination of a separately-tracked physics engine and the versatility of Lua will, when used correctly, manage this information for you.

(For more resources related to this topic, see here.)

What do we build?

We'll explore this idea of managing progress through a game schedule by completing a scrolling shooter in the tradition of games such as Xevious or River Run, where the player maneuvers a ship around the screen, avoiding enemy fire and destroying enemy ships until he/she reaches a boss target. Enemies will include ships that fly various patterns across the screen, turrets that follow the scrolling background and turn to face the player's ship, and a boss at the end that takes many hits to destroy and moves in various directions, firing multiple weapons. The player's ship will be able to fire at points on the screen that the player touches.

What does it do?

The player maneuvers their ship around the screen as the ground scrolls under them from the top of the screen downward. As the scrolling background reaches certain points, enemies appear and fly or scroll around the screen, firing bullets as they go; the player must avoid the bullets as well as the ship itself while shooting back.

Like the Deep Black game, this project will be based on Corona's Box2D-based physics library. In this version, most objects will be "sensors", meaning they only detect collisions, and do not bounce off of each other or transfer momentum. We'll also use Box2D's collision filters so that we don't need to process enemy ships colliding with each other, or bullets hitting each other.

In the TranslationBuddy project, we got a taste of coroutines and how they can be used to bookmark a task that you're in the middle of and come back to it later. In this project, we'll take that concept much further, using coroutines to create scripted behavior that's carried out over time, which is the simplest part of what game players and developers usually refer to as AI. We'll create computer-controlled fighters that follow flight assignments which are predictable at run time, but easily customized by the designer.

Why is it great?

In addition to coroutines, we'll also use a Lua feature called environments to create a minimal language of very simple functions controlling intervals and enemy actions. Features like this can become useful in larger projects, where programmers and designers must collaborate on a project. In such projects, programmers are responsible for the code that carries out the actions which the enemies will take, but the decisions of what the enemies should do, when, and in what order, are made by game designers, and frequently have to be adjusted for best balance and fun. For this reason, it's good to let the designers edit these schedules and scripts themselves. Designers are usually not experienced programmers, although they often have a little knowledge of programming and scripting, so this simple language will make it much easier for the designers.

How are we going to do it?

For this project, we'll review the design, rather than creating it. While an indie programmer will often be designing their own games and then coding from their designs, any programmer at a studio is likely to be coding from a document given to them by a designer or design team. We will be managing game progress with the help of the following game coroutines:

  • Founding the framework

  • Moving the player

  • Scheduling enemies

  • Scripting behavior

  • Controlling the boss

  • Cleaning up and making the game playable

What do I need to get started?

First, open the file design.txt from the project pack and read through it, noting the [NYI] tags that indicate features still pending. In full development projects, this sort of status tracking will usually be carried out by a more complex database or dedicated tracking program, but even in small projects, a simple record of what has yet to be accomplished can be very useful.

At this point, the game has files describing various ships (a broad category which also includes the ground-based, immobile turrets) as well as the various weapons with which the ships are equipped, the bullets they fire, and the explosions when they land. It also includes code that handles things taking damage—including events that can be tracked by user interface elements or gameplay progress tracking, processing user input into commands, and a long background made up of large tiles. A splash screen is already completed and appears when the game is launched.

What the game still needs is actual level design. Enemies need to appear as time advances in the level, and carry out various plans of attack against the player. This means that there are two kinds of schedules required; the schedule of which enemies appear when, and the individual schedules of the enemies that dictate how each one flies and attacks after it is created. To make this happen, we'll not only create scheduling behaviors, but also modules that attach these behaviors and other suitable characteristics, such as orientation, to our predefined ships to make them into bosses and enemies.

This distinction between units in a game that have statistics and behaviors, and the actors that represent them in the game world, is very powerful for scaling projects up in complexity.

Once that's done, create a new project folder, Atmosfall, and copy the contents of the version 0 folder in the project pack directory. You should be able to load this project into the simulator and advance the game past the splash screen, seeing a ship in the middle of a swamp background. In the simulator, you can click anywhere on the game screen and watch the ship fire double bullets at the point at which you click. You can also build the game for a device and watch the ship slide around the screen as you tilt the device. The prototype responds to user input and is ready to start adding the scheduling and AI enemy control.

Tracking progress through the level

Games like this typically trigger enemy appearances and events based on how far the background has scrolled past the screen. To focus on the challenges of the project, we'll import the background itself and just add the scrolling logic to issue events that track this progress.

Getting ready

You should have already copied the partly completed project from the version 0 folder into your new project directory; if you haven't, do that now.

Getting on with it

We'll start by loading the new marsh background into the Ground layer of the game's view, instead of the blank rectangle that the project uses by default. Open the game.lua file and change the createScene function to load this module as the new background, as shown in the following code snippet:

local group = self.view
self.Ground = require "level.marsh"(group)
self.Mobs = display.newGroup()

Then we adjust the scale of the background to make it fit into the width of the screen:

self.Ground = require "level.marsh"(group)
local scale = display.contentWidth / self.Ground.width
self.Ground.xScale, self.Ground.yScale = scale, scale

self.Mobs = display.newGroup()

If you test the code at this point, you should see a static background filling the screen behind the player ship.

Sliding the background

Pinning a moving rectangle to another moving rectangle so that it doesn't slide too far and show the back side of the world requires a little bit of math, but we can offload this math onto Corona with a little ingenuity.

First, when the scene starts, we'll make sure that in the willEnterScene responder, the background is lined up, with its bottom edge along the bottom edge of the screen.

physics.setGravity(0, 0)
self.Ground:setReferencePoint(display.BottomCenterReferencePoint)
self.Ground.xOrigin, self.Ground.y = display.contentCenterX,
display.contentHeight

for _, coordinates in ipairs(walls) do

Before we can start the background moving, we need to know how long it's supposed to take. This could vary from level to level in a real game, so we will let the schedule we load provide this value (the file in question doesn't exist yet; creating it will be part of our next task, so make a note that it will need to return its total length) as shown in the following code:

physics.setGravity(0, 0)
self.Duration = require "level.marsh-enemies" (self)
self.Ground:setReferencePoint(display.BottomCenterReferencePoint)

Now that we know the desired length of the background scroll, we can start the transition in response to the enterScene event, once the scene has finished loading as shown in the following code snippet. This is where the magic happens and the explanation will follow once you've had a chance to scan it.

self.Exit = nil
local bounds = self.Ground.contentBounds
self.Ground.Pan = transition.to(self.Ground,
{
time = self.Duration * 1000;
y = 0, yReference = bounds.yMin - bounds.yMax;
onComplete = function(object)
object.enterFrame, object.Pan = nil, nil
end
}

)
self.Lives = 2

The key here is that transition.to (and transition.from) can tween any value on any object that can be indexed, even though it usually gets applied to the visible properties of display objects. The yReference value has no visible effect on an object by itself; changing an object's yReference value changes what value its y position ends up being in its parent coordinates, but doesn't cause anything to move on the screen. However, when you combine this with continually adjusting the y position, it has the effect of changing the scale of the motion; setting the y position moves the object so that the point designated internally as its yReference value sits at the specified coordinate in its parent system.

We also needed to know the distance to move the reference point in the group; the marsh module builds a group with its zero point, or origin, at its bottom. So eventually, we need to move the yReference value to a negative value equal to the functional height of the background. We figure this out from the height of its bounding rectangle.

If you want to test this out, you'll have to temporarily replace self.Duration = require "marsh-enemies" (group) with self.Duration = 60. To temporarily sub out values like this, I often create an end-of-line comment, making the line look like the following:

self.Duration = 60 -- require "level.marsh-enemies" (self)

Then, to replace the old code, I can just delete the part that says 60 ––. Once you've inserted this place-holder, you can test the code and you'll see the background crawl past. However, if you're using a tall profile device, such as an iPhone 5, you may see that the bottom edge of the background first creeps down across the empty bar at the bottom of the screen, before filling it completely, and at the end, it will expose a bar of black at the top as it creeps into place. This is because when Corona uses letterbox alignment, it only positions the reference frame for drawing to leave space at the edges; it doesn't actually crop out anything that was hanging over the edges of the screen. But we can do this fairly easily by adding a mask to the display stage, the way we did on scenes in Project 3 to hide bits of overhanging text during transitions. To enforce this letterboxing globally throughout the program, we can add the mask in main.lua.

require "input"
display.currentStage:setMask(graphics.newMask("effect/masking-frame.
png"))
display.currentStage.maskX, display.currentStage.maskY = display.
contentCenterX, display.contentCenterY

local storyboard = require "storyboard"

Tracking the background progress

We need to track how far the schedule for the level will have advanced. We want this to be as fine-grained as we can manage, so we'll check the value as frequently as Corona will do, which is every frame. This means that an enterFrame listener is the logical vector which is shown in the following code snippet:

Runtime:addEventListener('enterFrame', self.Ground)
function self.Ground:enterFrame(event)
end

self.Lives = 2

For the schedule, we need to track time elapsed since the schedule starts. We'll make a note of the time when the scene begins, and post the Progress events that indicate what time the schedule has reached at, as shown in the following code:

self.Ground.Start = system.getTimer()
Runtime:addEventListener('enterFrame', self.Ground)
function self.Ground:enterFrame(event)

The other thing we want to track is the actual motion of the background. The first enemy we'll create is a turret, so if it doesn't appear to move in sync with the ground, the user will find that very distracting. This is a little trickier, since the ground's position and reference are both moving on the same sliding scale. Fortunately, there is another point that moves more predictably—the origin.

The origin of an object is whichever point is considered (0, 0) for placing that object's reference point. For most objects, it's fixed at the object's center; for groups, it's (0, 0) in the group's coordinates, wherever the group's children happen to be placed in relation to it. In our case, what we're really concerned with is that the object's origin is fixed with respect to its visible contents in a way that the reference point isn't. So if the group moves down by three pixels on the screen, you can say confidently that its yOrigin value also increased by three (assuming that it's not parented to a group with a different yScale value).

We can track the position of the ground's yOrigin value from frame to frame in order to determine how much it has visibly moved. We can include that information in the events we dispatch to supply the progress information as shown in the following code:

self.Ground.Start = system.getTimer()
self.Ground.oldY = self.Ground.yOrigin
Runtime:addEventListener('enterFrame', self.Ground)
function self.Ground:enterFrame(event)
scene:dispatchEvent{name = 'Progress'; time = (event.time - self.
Start) / 1000; delta = self.yOrigin - self.oldY}
self.oldY = self.yOrigin

end

Our schedule module will feed off these events to update its progress and trigger actions. First, however, we need something for the schedule module to do; schedules for levels will consist of spawning enemies at different intervals.

Constructing the enemy behavior

For the schedule to spawn new enemies, we need one to exist. We'll start with a simple one, that just moves in sync with the ground to start with. Once that works, we'll add tracking and weapon fire..

Getting on with it

Add a new file, turret.lua, and open it. This file will add turret behavior to a ship sprite and physics description specified in the starting project.

Creating an enemy

Add the basic description of the turret object's appearance and physics as shown in the following code:

local ship = require "ship.ship"
local category = require "category"
local groundFilter = {
groupIndex = category.enemy;
}
return function(game, x, y)
local self = ship.turret(game, game.Mobs.Ground, groundFilter)
self.x, self.y = x, y
self.bodyType = 'static'
return self
end

This creates a turret that just sits perfectly still and does nothing, which isn't very interesting. The next thing we'll do is make it move in sync with the ground by following the Progress events that we added to the game in the last section as shown in the following code:

self.bodyType = 'static'
game:addEventListener('Progress', self)
function self:Progress(event)
if self.y then
self.y = self.y + event.delta
else
game:removeEventListener('Progress', self)
end
end

return self

Since each Progress event contains the amount the screen moved, we can move the turret by the same amount. Now that this object has some basic behavior, it's time to start linking it into the game and at aching the scheduling mechanism. Save and switch to game.lua, and add a simple table before the event definitions using the following code:

local scene = storyboard.newScene()
scene.Spawn = {
turret = require "turret";
}

function scene:createScene( event )

This means that we can call game.Spawn.turret(game, x, y) to create a new turret at x, y in the world for game. However, we're not going to call it directly. We'll create a schedule that contains functions to spawn enemies in a common, shared context (the game) to save the code having to contain the same values repeated an awful lot.

Creating a schedule

In order to have our turret appear at the right point, we'll create a schedule that spawns new enemies as the level progresses.

Getting on with it

Save game.lua for the moment and create a new file in the level folder called marsh-enemies.lua. This file will define a schedule module that's 60 seconds long, so that's the first thing we'll define using the following code:

return function(game)
local duration = 60
return duration
end

At this point, if you previously put a placeholder duration in game.lua to test the background scroll, you can revert that to the code that uses this file.

Next we'll use the schedule function (from a module that isn't created yet) to start our custom schedule function against the current game. This will take a few steps to make it fully clear, but showing you how it will be used in the following code should help you see why this setup is worth engineering:

local duration = 60
schedule(game,
function()
at (0.3) spawn.turret(50, -20)
end
)

return duration

Now here's the catch—the at and spawn.turret functions haven't been defined anywhere yet, and they won't be made local in this file or defined as globals, even though this function uses them as global. Our schedule function will create them in a custom environment.

So for each schedule function, we'll create an environment that contains simplified actions on the game the schedule is for, and use it for the function that defines the schedule. We'll combine this with making the schedule function part of a coroutine, so that it can suspend itself, such as when it is waiting for a particular time to come up in the schedule.

So, before moving on, load the module you're about to create into marsh-enemies.lua using the following code:

local schedule = require "schedule"
return function(game)
local duration = 60

Building a schedule framework

Save marsh-enemies.lua and create the file schedule.lua at the top of your project. This le won't actually be very long. The core is a function that starts each newly created coroutine, at aching the environment supplied to the schedule, running that schedule until it's complete, and finally disconnecting the schedule from the game so that it won't throw errors or take up processing time as shown in the following code:

local function bind(game, listener, actions, schedule)
setfenv(schedule, actions)
schedule()
game:removeEventListener('Progress', listener)
end

The rest of the module will be a function that does the work of setting it up. It'll create an environment that contains bridge actions to the main game, start a coroutine using our glue function, and start that coroutine with the schedule function and environment. This coroutine will wake up every time the game object receives a Progress event to see if there are any enemies it needs to spawn, create those required, and go back to waiting.

We'll start by creating a blank environment and our coroutine:

game:removeEventListener('Progress', listener)
end
return function(game, schedule)
local actions = {}
local self = coroutine.wrap(bind)
end

Unlike coroutine.create, coroutine.wrap returns a function that resumes the new coroutine each time it's called. It's usually a little more convenient, but be a little careful with coroutine.wrap because if any error is thrown inside the coroutine, it will bubble right up and affect the code calling the resume function.

We'll then connect the new coroutine to be resumed for each Progress event sent to the game using the following code:

local self = coroutine.wrap(bind)
game:addEventListener('Progress', self)
end

We'll create a listener that stops feeding new Progress events into the schedule module when the game ends, such as when the player loses all of his or her lives as shown in the following code. This efiectively terminates the schedule; there's no way anymore to resume it and it will get garage-collected.

game:addEventListener('Progress', self)
local function close(event)
if event.action == 'ended' then
game:removeEventListener('Progress', self)
game:removeEventListener('Game', close)
end
end
game:addEventListener('Game', close)

end

Then, we'll start the coroutine with its schedule and environment, as well as the information it needs to clean itself up, and return the new coroutine in case the calling code has some use for it:

game:addEventListener('Game', close)
self(game, self, actions, schedule)
return self

end

Building the scheduled actions

Of the two functions we've described, the at action is the simpler one. It checks the elapsed time in the schedule module; if it's not enough, it yields to keep waiting, but if its designated time has arrived, it returns from its loop and lets the schedule advance. This means the code is very straightforward as follows:

local actions = {}
function actions.at(time)
repeat
local progress = coroutine.yield()
until progress.time >= time
end

local self = coroutine.wrap(bind)

Calling the function at may seem a little strange, but it allows the schedule calls to read much more like normal language. We could have called the function waitUntil and written the function waiting for it on another line, but Lua's loose syntax allows us to use this very compact format.

The spawn functions are a little more complex. We could simply build them all like the following:

function actions.spawn.turret(x, y)
return game.Spawn.turret(game, x, y)
end

However, since each one would follow the same pattern, we can use another pattern based on metatables and the __index lookup, the self-populaing table. __index on a table's metatable is only used when the requested key isn't in the original table; this means that the function that creates the requested value can store it in the table, and next time, it will simply be retrieved from the table instead of being looked up in the __index table again. This makes the spawn action family easy to summarize in one block.

actions.spawn = setmetatable({},
{
__index = function(t, name)
local function cue(...)
return game.Spawn[name](game, ...)
end
t[name] = cue
return cue
end
}
)

local self = coroutine.wrap(bind)

That's actually all there is to the schedule system! You can test it out and watch the turret appear in the upper-left hand slide-down corner across the screen, mindlessly facing right and doing nothing.

Bringing an enemy to life

Making the turret work requires only two main steps. The first is to have it track the player. Since the game object keeps a reference to the Player object, this boils down to basic trigonometry.

Open turret.lua and add a line to the Progress handler as shown in the following code:

if self.y then
self.y = self.y + event.delta
self.rotation = math.deg(math.atan2(game.Player.y - self.y,
game.Player.x - self.x))

else

The arctangent of the y and x distances gives the facing direction from the turret to the player, which we can use as the rotation angle to make the turret point at the player. If you try the code again you should see the turret stay pointed at the player as it scrolls down and the player moves. The last step is to give the turret a weapon. While we have the turret file open, have it start firing as soon as it is created:

end
self.Weapons.AntiAir:start(self, 100, 0)
return self

This depends on the turret having a weapon called AntiAir, which in this case was already created in the existing partial code.

The turret is complete for the time being. If you test the code, it should shoot at the player, and bullets fired at it should vanish when they hit it. Currently, however, nothing gets destroyed no matter how many bullets hit it.

What did we do?

A lot of stuff happened in this section! Using coroutines to track a continuously advancing schedule of new enemies, using Lua environments to wrap up some complicated actions in some simple wrappers, and adding an advancing series of progression data to drive everything else.

What else do I need to know?

Environments are a powerful feature of Lua; each function has an associated environment which is just a table linked to that function. Whenever a function needs to use a global, it looks up the global name as a string key in that table. By changing a function's environment, we can give it access to a totally different set of global functions. The real power here will come from the fact that the environment that we link to our schedule will be stocked with functions that use the normal environment, and can therefore take actions whose particulars are hidden from the code using them.

Lua creates a default environment to link to its code when it's started, containing all the standard global functions, and stores a link to that environment in that environment under the name _G, which you've probably used already.

Corona SDK Hotshot A detailed guide with 10 projects specifically designed to expand the fundamentals of this exciting mobile development platform! with this book and ebook
Published: May 2013
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

Scripting behavior

An enemy that just sits and shoots predictably quickly stops presenting much of a challenge. Our next enemy will be capable of steering around the screen according to a predesigned path, but since the path will be managed through a schedule similar to the one we use to spawn enemies, a different path can be given to each enemy.

Getting ready

This new enemy will be a fighter craft equipped with a single forward-facing machine gun that can be turned on and off. It can face a given direction, set its speed, and adjust its facing over time to create more naturally curved paths. We'll use the third entry in ship/sprite. lua to represent this ship.

Getting on with it

Prepare a new file in the top level of the project called dogfighter.lua. It will start much like the player.lua and turret.lua files. Notice that this function takes an extra argument—the function that describes its orders as shown in the following code:

local ship = require "ship.ship"
local category = require "category"
local enemyFilter = {
groupIndex = category.enemy;
}
return function (game, x, y, path)
local self = ship.fighter(game, game.Mobs.Air, enemyFilter)
self.x, self.y = x, y
return self
end

Because the fighters will face down by default, we'll rotate it in the opposite direction from the player. To keep the shadows and highlights on the sprite still appearing to come more or less from the same side, we'll reverse the y scale.

self.x, self.y = x, y
self.rotation = 90;
self.yScale = -1;

return self
end

We'll set a Speed value that will hold the ship's desired scalar velocity, without regard to which direction it's going in or how that velocity will be broken down into the x and y speeds:

self.rotation = 90;
self.yScale = -1;
self.Speed = 0
return self
end

We'll use a plan module, to be created next, to generate a coroutine that will carry out the requested orders, similar to the schedule module:

self.Speed = 0
local plan = require "plan" (self, path)
return self

Finally, we'll start a timer to update this coroutine according to the passage of time, and store this timer so that we can stop it when the ship is destroyed or removed:

local plan = require "plan" (self, path)
self.Guidance = timer.performWithDelay(1000/application.fps, plan,
0)

return self

Writing a ship control script

To get a notion of what commands our plan module needs to support, let's lay out a control script first. The ship will start by facing down across the screen, move forward at a set speed, and start firing as it travels down. After a second, it will veer off to the left and stop firing. This function will be laid out in the marsh-enemies.lua module, so save dogfighter. lua and reopen that module to add the following code:

function()
at (0.3) spawn.turret(50, -20)
at (0.5) spawn.defender(140, -10,
function ()
face(90)
go(100)
fire("MachineGun", 50, 0)
after (1) turn(60, 0.75)
release("MachineGun")
end
)

end

So, this gives us at least six functions to support:

  • face() will instantly set the ship to face the specified direction.

  • go() will set the ship's forward velocity to the specified speed.

  • turn() will adjust the ship's facing by the specified angle relative to its current facing, over the specified amount of time.

  • after() will wait until the specified amount of time has passed since after() was called, then proceed. It's not quite the same as at() in the level schedule, because that waits for specific intervals from the start of the level.

  • fire() and release() will start or stop the specified weapon. The same key should be used that was used to identify the template in the ship's Weapons table.

The dogfighter module uses the plan module to support these functions, so save marsh-enemies.lua and create the new module, plan.lua, in the top level of the project. This file will start off with a skeleton similar to that of schedule.lua, as shown in the following snippet, with which it has a great deal in common:

local function bind(path, actions)
end
return function(self, path)
end

The plan will have the same sort of glue function, which will at ach a set of actions to the plan function in the same way:

local function bind(path, actions)
setfenv(path, actions)
path()

end

However, where schedules are intended to run for a fixed amount of time, a ship's orders need to be carried out as long as it's alive, which could be highly variable. But since we expect plans to run off of a timer that will repeat indefinitely, this makes them more uniform to cancel once the plan function is exhausted. We just wait for one more firing of the timer so that we can get the timer source to cancel it.

path()
local finalize = coroutine.yield()
timer.cancel(finalize.source)

end

The code to start a new plan from the glue function and arguments will be almost identical to that of schedule.lua:

return function(self, path)
local actions = {facing = self.rotation}
local plan = coroutine.wrap(bind)
plan(path, actions)
return plan

end

The ship control code will be able to refer to facing as a global variable to determine the angle at which the ship is pointing, although setting this global will not turn the ship. This is convenient for scripts that might want smarter behavior.

Defining ship actions

The easiest actions to implement will actually be the fire control ones:

local actions = {facing = self.rotation}
function actions.fire(name, xAim, yAim)
end
function actions.release(name)
end

local plan = coroutine.wrap(bind)

We'll put a sanity check in just so that if a designer (or programmer) misspells a weapon name or otherwise tries to fire a non-existent weapon, it doesn't crash the ship's entire plan:

function actions.fire(name, xAim, yAim)
if self.Weapons[name] then
self.Weapons[name]:start(self, xAim, yAim)
end

end

We'll take a similar approach with the release function, but as a convenience, it can be called without any name to stop all firing weapons:

function actions.release(name)
if name and self.Weapons[name] then
self.Weapons[name]:stop()
else
for name, weapon in pairs(self.Weapons) do
weapon:stop()
end
end

end

The go function is one of the functions that I was referring to when I mentioned that some functions get simpler when the base ship images are pointed to the right. It also stores the scalar speed to facilitate turning functions:

local actions = {facing = self.rotation}
function actions.go(speed)
local angle = math.rad(actions.facing)
self:setLinearVelocity(speed * math.cos(angle), speed * math.
sin(angle))
self.Speed = speed
end

function actions.fire(name, xAim, yAim)

The Corona rotation properties are measured in degrees, but Lua math functions return and expect angles in radians. The math.deg and math.rad functions help plug this gap.

Finally, we get to use turn(). This is a slightly more complicated function because it keeps control of the ship until the turn is complete. For convenience, it expects durations in seconds, not milliseconds, as shown in the following code:

self.Speed = speed
end
function actions.turn(angle, duration)
duration = duration * 1000
end

function actions.fire(name, xAim, yAim)

It needs to know how much time has passed since the last time it was checked, so it starts by noting the time it started:

function actions.turn(angle, duration)
duration = duration * 1000
local start = system.getTimer()
end

The function will continue reducing the duration by the elapsed time whenever it gets notified of time passing, until the complete duration has passed:

local start = system.getTimer()
while duration > 0 do
local elapsed = coroutine.yield()
elapsed, start = elapsed.time - start, elapsed.time
duration = duration - elapsed
end

end

To turn the ship smoothly, it will see what portion of the turn duration has just elapsed (maxing out at all of it), and turn by that large a slice of the remaining angle:

while duration > 0 do
local elapsed = coroutine.yield()
elapsed, start = elapsed.time - start, elapsed.time
local wedge = angle * math.min(1, (elapsed / duration))
actions.face(actions.facing + wedge)

duration = duration - elapsed
end

Finally, it will reduce the remaining angle to turn by the amount it just turned by:

actions.face(actions.facing + wedge)
angle = angle - wedge
duration = duration - elapsed
end

Adding a new ship to the level

Open game.lua and add the dogfighter module to the list of elements the game can create:

scene.Spawn = {
turret = require "turret";
dogfighter = require "dogfighter";
}

If you test the code at this point, you will see a ship fly down across the screen and bank, although it should not fire yet.

Adding weapon fire

Save and move to the file ship.lua in the ship folder of your project directory. Find the section where it declares the fighter description, and fill a new entry into the Weapons table:

MaxHealth = 1;
Weapons = {
MachineGun = weapon.MachineGun,
};
},
dreadnought = ship {

If you test the code again, the enemy ship should fire as it comes down from the top of the screen.

What did we do?

We expanded the principles used for the schedule module to run actions in an even more specific context by using a single ship as the basis for the environment applied to the new plan. This system opens up a lot of options for producing varied ship behaviors to challenge and engage the player. Ships can fly in formation, appear in waves, and use various fire patterns to spray the screen with bullets.

What else do I need to know?

There's one important caveat to using functions with environments like this, in Lua, functions are actually objects; this means that if you set different environments on a function, the environment more recently set is used. And if you have multiple references to a function, setting the environment on one of them changes all of them. This means that if you try to use the same function as a plan for more than one ship at once, very strange things may happen, as if all the guidance systems were interfering.

The easiest way to deal with this is to make plan factories, functions that return a new copy of a function each time they're called. Each such closure, or instance of function code, has its own upvalues and its own distinct environment. By the end of the project, we'll have constructed several of these.

Controlling the boss

No game like this is complete without an awe-inspiring boss craft or base that offers a more complex fight. Because boss crafts don't typically fly around in circles, a new action is called for in plan.lua; hover. It's very similar to turn in its implementation, so we won't cover it in too much detail.

Getting on with it

First, open plan.lua and add the hover function to the environment:

self.Speed = speed
end
function actions.hover(xMove, yMove, duration)
duration = duration * 1000
local start = system.getTimer()
while duration > 0 do
local elapsed = coroutine.yield()
elapsed, start = elapsed.time - start, elapsed.time
local fraction = math.min(1, (elapsed / duration))
local xDrift, yDrift = xMove * fraction, yMove * fraction
xMove, yMove = xMove - xDrift, yMove - yDrift
self.x, self.y = self.x + xDrift, self.y + yDrift
duration = duration - elapsed
end
end

function actions.turn(angle, duration)
duration = duration * 1000

Creating the boss object

Now that the AI script actions are complete, we'll create the file that will accept these actions and advance the game when a boss is destroyed. Make a copy of dogfighter.lua in the top level of the project and name it boss.lua. The following are the steps to create a boss object:

  1. The module function will accept a flight plan function like the dogfighter module does, but also accepts a function that dictates what will happen when the boss is defeated:

    return function (game, x, y, path, advance)
    local self = ship.dreadnought(game, game.Mobs.Air, enemyFilter)

  2. Record the advance function in the boss's table so that we can retrieve it and run it when the boss dies:

    self.Speed = 0
    self.Advance = advance
    local plan = require "plan" (self, path)

Using a supplied function gives us choices about how bosses appear. For instance, you could make a level where a boss appeared halfway through, and its defeat started a new schedule to proceed through the rest of the level.

Making the boss module available

In order for the schedule to be able to spawn a new boss object, the game's spawn table needs a reference to the module. Open game.lua and add a new line to the table near the top of the file.

scene.Spawn = {
turret = require "turret";
dogfighter = require "dogfighter";
boss = require "boss";
}

Driving the boss's behavior

To launch the boss at the end of the level, we'll modify the level schedule. Open marsh-enemies.lua and find the end of the function being passed to the schedule.

  1. We'll add an entry a second or two before the schedule runs out to create the boss object:

    at (58) spawn.boss(160, -40, orbit(), bossOneDestroyed)
    end
    )
    return duration

  2. Because it's easy in this case, we'll specify what happens when the boss is defeated first:

    end
    local function bossOneDestroyed(self, game)
    game:dispatchEvent{name = 'Game'; action = 'ended', outcome =
    'won'}
    end

    return function(game)

  3. Then we'll create a function that returns a set of flight orders moving in a figure-eight pattern; first, the boss will move down from its spawning point at the top of the screen and slide over to one corner of its loop:

    local function orbit()
    return function()
    hover(0, 100, 2)
    after (1.5) hover(55, 0, 1.5)
    end
    end

    local function bossOneDestroyed(self, game)

  4. Then, its flight plan will go into a loop that will last until it is destroyed, moving down, then up and over, and then the same on the other side.

    return function()
    hover(0, 100, 2)
    after (1.5) hover(55, 0, 1.5)
    while true do
    after(1) hover(40, 50, 1.5)
    after(1) hover(-150, -50, 4)
    after(1) hover(-40, 50, 1.5)
    after(1) hover(150, -50, 4)
    end

    end

  5. As the boss moves, it will switch some of its guns on and off as it changes direction. Because these calls are repetitive, the flight plan can define reusable functions for them.

    after (1.5) hover(55, 0, 1.5)
    local function crossFan()
    fire(1, 50, 80); fire(2, 50, -80)
    release(3); release(4)
    fire(5, 50, 13); fire(6, 50, -13)
    end
    local function focusForward()
    release(1); release(2)
    fire(3, 48, -15); fire(4, 48, 15)
    release(5); release(6)
    end

    while true do

    This ability to keep using loops, functions, and other programming semantics inside a sandbox like this makes it especially powerful.

  6. Once it calls those functions appropriately, the flight plan is complete.

    while true do
    focusForward()
    after(1) hover(40, 50, 1.5)
    crossFan()
    after(1) hover(-150, -50, 4)
    focusForward()
    after(1) hover(-40, 50, 1.5)
    crossFan()
    after(1) hover(150, -50, 4)
    end

Handling the boss's defeat

The default onDestroy handler falls short in two regards where the boss is concerned. It needs to end the level (as determined by the boss's advance function), and frankly, one little explosion isn't impressive enough for a boss's victory. The following are the steps to handle the boss's defeat:

  1. Open ship.lua and frame in an onDestroy override for the dreadnought entry:

    dreadnought = ship {
    sprite = chassis [4];
    MaxHealth = 1000;
    onDestroy = function(game, event)
    local self = event.unit
    end;

    Weapons = {

  2. Most of the functions are still the same as the default onDestroy function, so we'll start by bringing them in. However, we'll hold off on removing the boss sprite as shown in the following code:

    onDestroy = function(game, event)
    local self = event.unit
    timer.cancel(self.Guidance)
    for id, weapon in pairs(self.Weapons) do
    weapon:stop()
    end

    end

  3. First, we'll start a repeating timer to set off explosions continuously on top of the boss sprite:

    local w, h = self.width / 2, self.height / 2
    local chainReaction = timer.performWithDelay(100,
    function(event)
    explosion(self.parent, self.xOrigin + math.random(-w,
    w), self.yOrigin + math.random(-h, h))
    end,
    0
    )

  4. Then, we'll start a one-use timer that will clean things up. It will stop the endless explosions, remove the boss, and trigger the boss's Advance function.

    self.Destruction = timer.performWithDelay(1000,
    function(event)
    timer.cancel(chainReaction)
    self:Advance(game)
    self:removeSelf()
    end
    )

Test things out! Once you make it past the other enemies and dodge the boss's guns, you'll see the boss going down in a blaze of glory. Having your character be invulnerable during development makes things like this much easier to test out.

What did we do?

We expanded the selection of actions available to the enemy's AI code. We created a new object type with a continuously repeating set of controls and internal logic to its actions. We also used one timer to regulate another, in order to create a visually interesting effect with a fixed duration.

What else do I need to know?

If you want to create functions for your control coroutines, they need to be defined inside the function that contains the instructions. Otherwise, they'll be created without the special environment that provides their instructions, and they won't respond properly.

Summary

This was a serious endeavor! This system has several complex elements and uses all three major elements of advanced Lua: metatables, environments, and coroutines. It combines the special controls of smartphones with a classic retro style of arcade game play.

Resources for Article :


Further resources on this subject:


Corona SDK Hotshot A detailed guide with 10 projects specifically designed to expand the fundamentals of this exciting mobile development platform! with this book and ebook
Published: May 2013
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

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.

Books From Packt


Corona SDK application design
Corona SDK application design

Corona SDK Mobile Game Development: Beginner's Guide
Corona SDK Mobile Game Development: Beginner's Guide

Marmalade SDK Mobile Game Development Essentials
Marmalade SDK Mobile Game Development Essentials

Unity 3 Game Development Hotshot
Unity 3 Game Development Hotshot

BlackBerry Java Application Development
BlackBerry Java Application Development

Developing Mobile Games with Moai SDK
Developing Mobile Games with Moai SDK

 Kinect for Windows SDK Programming Guide
Kinect for Windows SDK Programming Guide

Building your First Mobile Game using XNA 4.0
Building your First Mobile Game using XNA 4.0


Your rating: None Average: 5 (1 vote)

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
D
3
p
9
A
N
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software