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 

Note

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

Note

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

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)

Tip

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

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.

Note

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.91.254