Important News: 26/10/2016 - Call for Presentations to Lua Devroom at FOSDEM 2017
Important News: 04/05/2016 - Community news #2
Important News: 11/12/2015 - Blog opening and contribution guide

Using Lua coroutines to create an RPG dialogue system

By Jeremy Clarke Apr 11 2016 07:31 Gamedev Reblogged Comments

Recently I've been working on an RPG with a friend, for whom coding is not their strong point. We're using the excellent LÖVE framework, so the whole game is written in Lua.

Scripting of dialogues, animations and in-game events is a hugely important aspect for any RPG, and I wanted to build a scripting system that's easy to use and doesn't require any knowledge about the rest of the game's engine, but is also powerful enough for me to extend with new functionality as needed. This post aims to show how a few simple Lua features can be combined to create a scripting environment that's pleasant to use.

First, let's take a look at the XSE language used in Pokémon modding, as this was probably my main point of inspiration. It has a very straightforward, imperative style, even though each instruction doesn't correspond to a single function in-game.

By this I mean, the whole game engine doesn't freeze just because you are talking to an NPC, however there are points at which the dialogue script cannot progress until the text animations have finished and the player has pressed the [A] button.

XSE Example

Another interesting tool is Yarn, a dialogue editor in which you connect nodes of text together to form complete conversations. It has variables, conditionals and custom commands which you can hook up to different parts of your engine to trigger animations and such. I'd say it's definitely worth checking out especially if you're using Unity or similar.

So how would we go about creating such a system in LÖVE without creating our own language or writing an interpreter for an existing language such as Yarn?

Part 1: Chaining Callbacks Together

The first thing we need is the ability to 'say' some text from inside a script, which boils down to setting a string and then waiting for the user to press a button before we resume execution of the script. The game should still be updating on every frame, even when text is being displayed.

In true JavaScript fashion, we could create an asynchronous API that looks a bit like this:

text = nil
callback = nil

function say(str, cb)
    text = str
    callback = cb
end

Our game logic & rendering code could look something like this:

function love.update(dt)
    if not text then
        -- player movement code
    end
end

function love.draw()
    -- code to draw the world goes here

    if text then
        love.graphics.print(text, 10, 10)
    end
end

function love.keypressed(key, isRepeat)
    if text and key == "space" then
        text = nil
        if callback then
            -- execute the next part of the script
            callback()
        end
    end
end

Then we could write a dialogue script that looks like this, potentially fetching it at runtime with a call to dofile() or something:

say("Hello there!", function ()
    say("How's it going?", function ()
        say("Well, nice talking to you!")
    end)
end)

This kind of code grows unwieldy very quickly. It's confusing for non-coders and also error prone (many places to miss out a comma or a closing bracket). You could try some variations such as giving a name to each function, but it still turns out quite unpleasant to work with because managing all those functions gets in the way of what matters: writing good dialogue and scenes. At this point we'd surely be better off writing a Yarn interpreter or using some other existing solution.

But this is not JavaScript, and we can do better!

Part 2: Using Coroutines

For the uninitiated, coroutines are chunks of code that can be jumped to much like functions. A coroutine can suspend itself (yield) at will, returning to the point at which it was called. At a later stage, the program can jump back into the coroutine and resume where it left off.

I suppose this puts them in a sort of middle ground between functions and threads. They are more powerful than functions, but you still have to manage them explicitly - you can't just leave them running in the background to do their own thing. Typically they are used to break up an intensive task into small bursts, so that the program can still function as normal (receive user input, print to console, etc.)

Hang on a minute, doesn't this sound a lot like what we want from the dialogue scripting system? Executing a single line and then suspending the script while we give control back to the game loop?

Let's see how we could achieve the same result as Part 1, only using a coroutine instead of a chain of callbacks.

text = nil
routine = nil

function say(str)
    text = str
    coroutine.yield()
    text = nil
end

function run(script)
    -- load the script and wrap it in a coroutine
    local f = loadfile(script)
    routine = coroutine.create(f)

    -- begin execution of the script
    coroutine.resume(routine)
end

The important difference here is the implementation of the say function. Instead of setting a callback for later use, we tell the current coroutine to yield. This means we can't call say directly from the main program, only from inside a coroutine. Also there is now a loader function which creates a new coroutine and tells it to run the script.

Next we need to rewrite love.keypressed to make it resume the coroutine on the press of the space bar.

function love.keypressed(key, isRepeat)
    if text and key == "space" then
        if routine and coroutine.status(routine) ~= "dead" then
            -- execute the next part of the script
            coroutine.resume(routine)
        end
    end
end

And finally, we can write a script that looks like this:

say("Hello there!")                -- the script suspends once here
say("How's it going?")             -- it suspends again here
say("Well, nice talking to you!")  -- it suspends for the 3rd time here

Part 3: Sandboxing and Advanced Usage

If we declare a global variable, 'n', we can create an NPC that remembers how many times the player has spoken to it.

say("Hey kid, I'm Mr. Red!")

if n == 0 then
    say("I don't believe we've met before!")
else
    say("You have spoken to me "..n.." times!")
end

n = n + 1

It's great that this works, because it does exactly what you would expect and it's super easy to use. However, there are some problems.

If all the variables are stored in the global environment, we risk running into naming collisions which at best will cause scripts to behave incorrectly and at worst could replace key functionality and crash the game.

Additionally, having our game's state scattered across a ton of globals makes things very difficult when we want to think about serialising the gamestate to produce a save file.

Fortunately Lua makes it easy to swap out the environment of a function for any table, using setfenv in Lua 5.1 or _ENV in Lua 5.2 or greater. We don't need to change our scripts at all, we just need to make sure that they still have access to the say function, by placing it in their environment (the game table below).

game = {}

function game.say(str)
    text = str
    coroutine.yield()
    text = nil
end

function run(script)
    local f = loadfile(script)
    setfenv(f, game)
    routine = coroutine.create(f)

    -- begin execution of the script
    coroutine.resume(routine)
end

It also might be helpful to have a script that is called once at startup, to initialise all the game variables to default values, or load them from a save file.

As far as animation goes, we can drop in a tweening solution like flux, along with a few helper functions which will allow us to pause the script until the animation completes.

game.flux = require "flux"

game.pause = coroutine.yield

function game.resume()
    coroutine.resume(routine)
end

and then we could tween a character to x = 800 with a script like this:

flux.to(myNpc, 2.0, { x = 800 }):ease("linear"):oncomplete(resume)
pause()

which yes, is a mouthful for non-coders, and it introduces an asynchronous aspect back into the scripting API. We would probably benefit from a custom animation system that's more more tailored to our game, but this hopefully goes to show how easy it is to make scripts that can interact with any other part of the engine.

What Next?

I hope I was able to teach some interesting ideas here! I wanted to share this because coroutines are something I've known about for a while, but until now I've never had a good reason to use them. I would be interested to know which other languages can be used to create a system like this.

Here are some things you might want to do next, to create a more full-featured RPG engine:

  • Add lock() and release(), so it's possible to display text while the player is moving, or stop the player from moving even when there is no text.
  • Add an ask(str, ...) function whereby the player can choose from a list of options (e.g. yes/no)
  • Download a level editor such as Tiled, or create your own. Try attaching some scripts to game objects such as buttons and NPCs. Relevant tutorial on using Tiled with LÖVE
  • Create an easy-to-use animation system with commands such as 'face X direction' or 'move N steps'
  • Add character portraits so that the player knows who's speaking (this might require you to add an extra parameter to say() or some new functions)
  • Consider how you would go about handling save data. How to distinguish it from data which is part of the gamestate but does not need to be saved permanently?

Using Tiled Maps in LÖVE

By Landon Manning Dec 21 2015 07:08 Gamedev Comments

This tutorial will show you how to set up your very own 2D world using only free and open source tools. The tools we will be going over are Tiled, a tile map editor; LÖVE, a 2D game framework; and Simple Tiled Implementation (STI), a Lua module I wrote that imports and renders Tiled maps using LÖVE. This tutorial assumes that you are familiar enough with both Tiled and LÖVE that you can create a simple map and run LÖVE programs. If you have never used or heard of these program before, I highly recommend downloading them and tinkering with them a bit before continuing.

To begin, we want to create a simple map using Tiled. You can create your own tile atlas using any image editor, or download a free one from Open Game Art. In my example map, I have created a map with several tile layers and a single object layer with an object named "Player". This object will be used later on to spawn our player so make sure you create and name at least one object.

Tiled map editor

With our map created, we want to export it as a Lua file by clicking File > Export or by pressing ctrl+E and selecting Lua files (*.lua). It is worth noting that Tiled's default compression method is Base64. If you are using a version of LÖVE prior to 0.10, you will need to make sure that the map compression is set to CSV. It is also worth noting that Tiled formats image paths relative to the map's location. You need to make sure that you save your map in the same directory structure that your game will be loading with, or else you might have trouble loading your map.

Now that we have our map export ready, it is time to start writing some code. We will start by creating main.lua and putting in a basic LÖVE skeleton. This skeleton will allow us to load our map and other assets, update our map every frame, and render the map to our screen.

function love.load()
end

function love.update(dt)
end

function love.draw()
end

With our skeleton made, we want to import the STI module, load the map, and get it drawing on screen. It is surprisingly easy to do as STI takes care of a lot of the work for you such as loading (and caching) images, batching the tiles for faster drawing, etc.

-- Include Simple Tiled Implementation into project
local sti = require "sti"

function love.load()
    -- Load map file
    map = sti("map.lua")
end

function love.update(dt)
    -- Update world
    map:update(dt)
end

function love.draw()
    -- Draw world
    map:draw()
end

STI rendering a map

Amazing! We added just four lines of code and now our map is being drawn! But hey, what is that ugly box? That is our Player object that we created. Obviously we don't want that to be a box so let's create a player object.

First we need to add a new Custom Layer to our map. This layer will be used for holding dynamic data that lives in our world such as players, doors, or other objects that can get added, modified, or removed throughout the lifecycle of the game.

function love.load()
    -- Load map file
    map = sti("map.lua")

    -- Create new dynamic data layer called "Sprites" as the 8th layer
    local layer = map:addCustomLayer("Sprites", 8)
end

With our new layer added, we want to populate it with some data. We want to take the X and Y position from our map object and give it to a new dynamic object that we will call player. We also want to load an image to use as the player's sprite, and we want to offset that image when it is drawn so the player's position is at the sprite's feet. The offset values I used here worked for my image, but you may need to experiment to get the right values for your sprite.

It is worth noting here that Tiled supports adding custom properties to any object or tile. You could store the offset values as object properties and then grab them from object.properties in Lua. In my example below, I simply hard coded the ox and oy values to keep things simple.

function love.load()
    -- Load map file
    map = sti("map.lua")

    -- Create new dynamic data layer called "Sprites" as the 8th layer
    local layer = map:addCustomLayer("Sprites", 8)

    -- Get player spawn object
    local player
    for k, object in pairs(map.objects) do
        if object.name == "Player" then
            player = object
            break
        end
    end

    -- Create player object
    local sprite = love.graphics.newImage("sprite.png")
    layer.player = {
        sprite = sprite,
        x      = player.x,
        y      = player.y,
        ox     = sprite:getWidth() / 2,
        oy     = sprite:getHeight() / 1.35
    }
end

With our player object created, we want to draw it on screen. Every layer in STI has its own update and draw callback functions that can be overridden if necessary. Each layer type has a default callback that can be accessed through STI's API except Custom Layers which have empty callbacks. To get our player to draw, we are going to need to override our new layer's draw callback.

We are also going to want to remove our old Object Layer since we've extracted all the data we need out of it. If we wanted to keep it around, we could set layer.visible to false to hide the ugly boxes, but we're just going to kill it.

function love.load()
    -- Load map file
    map = sti("map.lua")

    -- Create new dynamic data layer called "Sprites" as the 8th layer
    local layer = map:addCustomLayer("Sprites", 8)

    -- Get player spawn object
    local player
    for k, object in pairs(map.objects) do
        if object.name == "Player" then
            player = object
            break
        end
    end

    -- Create player object
    local sprite = love.graphics.newImage("sprite.png")
    layer.player = {
        sprite = sprite,
        x      = player.x,
        y      = player.y,
        ox     = sprite:getWidth() / 2,
        oy     = sprite:getHeight() / 1.35
    }

    -- Draw player
    layer.draw = function(self)
        love.graphics.draw(
            self.player.sprite,
            math.floor(self.player.x),
            math.floor(self.player.y),
            0,
            1,
            1,
            self.player.ox,
            self.player.oy
        )

        -- Temporarily draw a point at our location so we know
        -- that our sprite is offset properly
        love.graphics.setPointSize(5)
        love.graphics.points(math.floor(self.player.x), math.floor(self.player.y))
    end

    -- Remove unneeded object layer
    map:removeLayer("Spawn Point")
end

Player sprite being rendered on map

Whoa, there it is! Our player sprite! "But wait!" you might be saying, "you said this data was dynamic! Our object is just sitting there!". Well, you're right. This is a player object and we need to give it some controls. I am going to ignore the great advice from a previous Lua Space article for the sake of simplicity, but I do not recommend writing player controller code like this. Definitely check that article out!

In our player controller, we will be overriding the layer.update callback to detect if certain keys are currently being pressed and if so, update the player's x and y positions by some predetermined speed value, in our case, 72 pixels per second, or three tiles. The key here is the per second bit. Since lots of computers run at lots of different speeds and frame rates, we can't hard code our per-frame values. We will be multiplying our speed by dt or Delta Time, a speed normalizer that LÖVE provides us. dt ensures that no matter how fast or slow the system running our game is, objects will move the same per-second distance.

function love.load()
    -- Load map file
    map = sti("map.lua")

    -- Create new dynamic data layer called "Sprites" as the 8th layer
    local layer = map:addCustomLayer("Sprites", 8)

    -- Get player spawn object
    local player
    for k, object in pairs(map.objects) do
        if object.name == "Player" then
            player = object
            break
        end
    end

    -- Create player object
    local sprite = love.graphics.newImage("sprite.png")
    layer.player = {
        sprite = sprite,
        x      = player.x,
        y      = player.y,
        ox     = sprite:getWidth() / 2,
        oy     = sprite:getHeight() / 1.35
    }

    -- Add controls to player
    layer.update = function(self, dt)
        -- 96 pixels per second
        local speed = 96

        -- Move player up
        if love.keyboard.isDown("w") or love.keyboard.isDown("up") then
            self.player.y = self.player.y - speed * dt
        end

        -- Move player down
        if love.keyboard.isDown("s") or love.keyboard.isDown("down") then
            self.player.y = self.player.y + speed * dt
        end

        -- Move player left
        if love.keyboard.isDown("a") or love.keyboard.isDown("left") then
            self.player.x = self.player.x - speed * dt
        end

        -- Move player right
        if love.keyboard.isDown("d") or love.keyboard.isDown("right") then
            self.player.x = self.player.x + speed * dt
        end
    end

    -- Draw player
    layer.draw = function(self)
        love.graphics.draw(
            self.player.sprite,
            math.floor(self.player.x),
            math.floor(self.player.y),
            0,
            1,
            1,
            self.player.ox,
            self.player.oy
        )

        -- Temporarily draw a point at our location so we know
        -- that our sprite is offset properly
        love.graphics.setPointSize(5)
        love.graphics.points(math.floor(self.player.x), math.floor(self.player.y))
    end

    -- Remove unneeded object layer
    map:removeLayer("Spawn Point")
end

Cool, we can now move our player around the screen! But that's only half the battle. We also want to centre the player in the screen so that the player never runs away from us. Instead, we want the world to move around our player. How do we accomplish this? LÖVE provides graphics transform tools such as translate, rotate, and scale that will give us the illusion that our player is static and the world is dynamic, instead of the other way around.

function love.draw()
    -- Translate world so that player is always centred
    local player = map.layers["Sprites"].player
    local tx = math.floor(player.x - love.graphics.getWidth() / 2)
    local ty = math.floor(player.y - love.graphics.getHeight() / 2)
    love.graphics.translate(-tx, -ty)

    -- Draw world
    map:draw()
end

Player sprite always rendered in the centre of screen

And there we have it, our player can walk around the world and always remain in the centre of our screen. We can also scale our world so that players using screens with different resolutions all see the same stuff instead of maybe having an advantage because your screen resolution is larger than someone else's, or having graphical glitches, or any other issue that could arise.

function love.draw()
    -- Scale world
    local scale = 2
    local screen_width = love.graphics.getWidth() / scale
    local screen_height = love.graphics.getHeight() / scale

    -- Translate world so that player is always centred
    local player = map.layers["Sprites"].player
    local tx = math.floor(player.x - screen_width / 2)
    local ty = math.floor(player.y - screen_height / 2)

    -- Transform world
    love.graphics.scale(scale)
    love.graphics.translate(-tx, -ty)

    -- Draw world
    map:draw()
end

World being rendered at 2x scale

Alright, now let's put it all together and see what we've got!

-- Include Simple Tiled Implementation into project
local sti = require "sti"

function love.load()
    -- Load map file
    map = sti("map.lua")

    -- Create new dynamic data layer called "Sprites" as the 8th layer
    local layer = map:addCustomLayer("Sprites", 8)

    -- Get player spawn object
    local player
    for k, object in pairs(map.objects) do
        if object.name == "Player" then
            player = object
            break
        end
    end

    -- Create player object
    local sprite = love.graphics.newImage("sprite.png")
    layer.player = {
        sprite = sprite,
        x      = player.x,
        y      = player.y,
        ox     = sprite:getWidth() / 2,
        oy     = sprite:getHeight() / 1.35
    }

    -- Add controls to player
    layer.update = function(self, dt)
        -- 96 pixels per second
        local speed = 96

        -- Move player up
        if love.keyboard.isDown("w") or love.keyboard.isDown("up") then
            self.player.y = self.player.y - speed * dt
        end

        -- Move player down
        if love.keyboard.isDown("s") or love.keyboard.isDown("down") then
            self.player.y = self.player.y + speed * dt
        end

        -- Move player left
        if love.keyboard.isDown("a") or love.keyboard.isDown("left") then
            self.player.x = self.player.x - speed * dt
        end

        -- Move player right
        if love.keyboard.isDown("d") or love.keyboard.isDown("right") then
            self.player.x = self.player.x + speed * dt
        end
    end

    -- Draw player
    layer.draw = function(self)
        love.graphics.draw(
            self.player.sprite,
            math.floor(self.player.x),
            math.floor(self.player.y),
            0,
            1,
            1,
            self.player.ox,
            self.player.oy
        )

        -- Temporarily draw a point at our location so we know
        -- that our sprite is offset properly
        love.graphics.setPointSize(5)
        love.graphics.points(math.floor(self.player.x), math.floor(self.player.y))
    end

    -- Remove unneeded object layer
    map:removeLayer("Spawn Point")
end

function love.update(dt)
    -- Update world
    map:update(dt)
end

function love.draw()
    -- Scale world
    local scale = 2
    local screen_width = love.graphics.getWidth() / scale
    local screen_height = love.graphics.getHeight() / scale

    -- Translate world so that player is always centred
    local player = map.layers["Sprites"].player
    local tx = math.floor(player.x - screen_width / 2)
    local ty = math.floor(player.y - screen_height / 2)

    -- Transform world
    love.graphics.scale(scale)
    love.graphics.translate(-tx, -ty)

    -- Draw world
    map:draw()
end

With less than 100 lines of code, we've imported our map, created a player object, created a player controller, and can run around our world. Impressive!

STI has other features not covered by this tutorial such as a plugin system and several collision plugins. If you would like to learn more about STI, you can check it out on GitHub, read the documentation, or visit the support thread on the LÖVE forums.


Handling Input In Lua

By undef Dec 11 2015 23:42 Gamedev Comments

In every interactive program you make you will have to process user input.

In bigger programs or games this can get quite confusing when you have many different states that require input to be handled differently. Even in a small game you will already have to handle input differently when the player is in a menu or on the title screen. If you scale up your game without using a reasonable code structure things will get really messy and work will get a lot harder.

To avoid your code from getting to messy your input handler will have to be:

  • simple
  • easy to read
  • extensible
  • able to easily handle multiple input devices

What I usually see in LÖVE projects is something like this:

function love.keypressed( k )
    if k=="escape" then
        love.event.quit()
    elseif k=="other key" then
    -- other keys
    end
end

This might be the way to go in some other programming languages, but in Lua we have the luxury of tables. One great thing about tables is that anything except nil can be index and anything can be value. That means that we can represent key presses with a corresponding list of statements as a table containing the key pressed (represented as string) as its index, and a function containing the statements as its value. That has the advantage that you don't need to compare the key pressed to a long list of potential key presses in a huge if-block to perform an aciton, indexing a table is sufficient.

This is the input handler I use in very small LÖVE projects:

local keys = {
    escape = love.event.quit,
    -- other keys
}

function love.keypressed( k )
    local action = keys[k]
    if action then  return action()  end
end

This is already much more neat, you can easily read the list of possible key presses and their effects. Notice that this pattern can be used on many different occasions. When ever you have a very long if-block and always compare the same variable to something, you can easily replace it with a table.

The key handler above is very small, simple and readable but doesn't satisfy everything we want from one. For small projects this is fine, because I usually only use the keyboard (even only love.keypressed), but it can also be easily extended to something more powerful:

local bindings = {
    backToGame = function() --[[<...>]]  end,
    scrollUp   = function() --[[<...>]] end,
    scrollDown = function() --[[<...>]] end,
    select     = function() --[[<...>]] end,
}

local keys = {
    escape     = "backToGame",
    up         = "scrollUp",
    down       = "scrollDown",
    ["return"] = "select", -- return is a keyword that's why it has to be written like this
}
local keysReleased = {}

local buttons = {
    back = "backToGame",
    up   = "scrollUp",
    down = "scrollDown",
    a    = "select",
}
local buttonsReleased = {}

function inputHandler( input )
    local action = bindings[input]
    if action then  return action()  end
end

function love.keypressed( k )
    local binding = keys[k]
    return inputHandler( binding )
end
function love.keyreleased( k )
    local binding = keysReleased[k]
    return inputHandler( binding )
end
function love.gamepadpressed( gamepad, button )
    local binding = buttons[button]
    return inputHandler( binding )
end
function love.gamepadreleased( gamepad, button )
    local binding = buttonsReleased[button]
    return inputHandler( binding )
end

So this is already a lot better, we can easily support multiple input devices like this. However usually games have different states with different input schemes, so it makes sense to integrate it into a state handler:

local state

local gameStates = {}

gameStates.menu = {
    bindings = {
        backToGame = function()  state = gameStates.gameLoop  end,
        scrollUp   = function() --[[<...>]] end,
        scrollDown = function() --[[<...>]] end,
        select     = function() --[[<...>]] end,
    },
    keys = {
        escape     = "backToGame",
        up         = "scrollUp",
        down       = "scrollDown",
        ["return"] = "select",
    },
    keysReleased = {},
    buttons = {
        back = "backToGame",
        up   = "scrollUp",
        down = "scrollDown",
        a    = "select",
    }
    buttonsReleased = {},
    -- <...>
}
gameStates.gameLoop = {
    bindings = {
        openMenu   = function()  state = gameStates.menu  end,
        jump       = function() --[[<...>]] end,
        left       = function() --[[<...>]] end,
        right      = function() --[[<...>]] end,
    },
    keys = {
        escape = "openMenu",
        lshift = "jump",
        left   = "left",
        right  = "right",
    },
    keysReleased = {},
    buttons = {
        back    = "openMenu",
        a       = "jump",
        dpleft  = "left",
        dpright = "right",
    }
    buttonsReleased = {},
    -- <...>
}

function inputHandler( input )
    local action = state.bindings[input]
    if action then  return action()  end
end

function love.keypressed( k )
    -- you might want to keep track of this to change display prompts
    INPUTMETHOD = "keyboard"
    local binding = state.keys[k]
    return inputHandler( binding )
end
function love.keyreleased( k )
    local binding = state.keysReleased[k]
    return inputHandler( binding )
end
function love.gamepadpressed( gamepad, button )
    -- you might want to keep track of this to change display prompts
    INPUTMETHOD = "gamepad"
    local binding = state.buttons[button]
    return inputHandler( binding )
end
function love.gamepadreleased( gamepad, button )
    local binding = state.buttonsReleased[button]
    return inputHandler( binding )
end

As you can see, both the escape key and the back button map to the binding backToGame, while the player is in the menu. When the binding backToGame is being sent to the input handler, the function state.bindings.backToGame is being called, resulting in the following assignment: state = gameStates.gameLoop. Now the input callbacks and the input handler will use gameStates.gameLoop to look for bindings and for functions to execute.


RSS

Subscribe to Lua.Space by Email