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

Note

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)

Tip

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.

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

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