In order to have our turret appear at the right point, we'll create a schedule that spawns new enemies as the level progresses.
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
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
Save marsh-enemies.lua
and create the file schedule.lua
at the top of your project. This file won't actually be very long. The core is a function that starts each newly created coroutine, attaching 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 effectively 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
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)
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-populating 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 and slide-down corner across the screen, mindlessly facing right and doing nothing.
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.
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.
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.
3.145.91.254