21 min read

(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.

Image

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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here