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:

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.

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:

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.

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.

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:

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:

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:

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:

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:

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:

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 = 60return durationend

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:

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:

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:

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.

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:

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.

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:

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:

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.

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:

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:

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

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:

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.

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:

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)endreturn 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.

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:

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:

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:

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.

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:

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:

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.

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:

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:

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

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.

Alerts & Offers

Series & Level

We understand your time is important. Uniquely amongst the major publishers, we seek to develop and publish the broadest range of learning and information products on each technology. Every Packt product delivers a specific learning pathway, broadly defined by the Series type. This structured approach enables you to select the pathway which best suits your knowledge level, learning style and task objectives.

Learning

As a new user, these step-by-step tutorial guides will give you all the practical skills necessary to become competent and efficient.

Beginner's Guide

Friendly, informal tutorials that provide a practical introduction using examples, activities, and challenges.

Essentials

Fast paced, concentrated introductions showing the quickest way to put the tool to work in the real world.

Cookbook

A collection of practical self-contained recipes that all users of the technology will find useful for building more powerful and reliable systems.

Blueprints

Guides you through the most common types of project you'll encounter, giving you end-to-end guidance on how to build your specific solution quickly and reliably.

Mastering

Take your skills to the next level with advanced tutorials that will give you confidence to master the tool's most powerful features.

Starting

Accessible to readers adopting the topic, these titles get you into the tool or technology so that you can become an effective user.

Progressing

Building on core skills you already have, these titles share solutions and expertise so you become a highly productive power user.