Gary's Lament — a text unadventure

Hi there. I’m Kay!

I decided to start a dev blog about this because I’ve never written much about my dev work before. I’ve always thought Knuth’s literate programming was really interesting and maybe this will be a little like it. I’m a wordy girl so you might want to skim.

Some background

Remember this? Lavender town. I don’t know why but I always remember the first gen Pokémon games as really bleak. Aesthetically they seem so… sparse. This is in contrast to the anime which I remember as very upbeat. And the glitchiness too. I don’t know — I just get a feeling from them.

& — Do you remember how in the first-gen (& presumably later gens) Pokémon games you could just go around and walk into people’s houses and talk to them? I find that so interesting as a writer. Same goes for Deus Ex — the stories are told through people and texts we couldn’t access usually in our everyday lives. I’m sure you can think of more examples of this ‘playable voyeurism’.

So I decided to appropriate the aesthetic from Pokémon to form some text-driven game(?) based on the places and people in my life.

First version

Because I’m new to game dev I decided to prove myself by writing my own game engine in JS using Redux. This has been a really interesting process and I’ve learned a lot through it. However, now I’m at the point where I’m starting to add NPCs and that would necessitate too much of a rewrite for me to be bothered with. That’s where Defold comes in. I’d have to rewrite anyway, and having a framework that enforces a bit more game dev history on me — I think that’ll help.

This is where I got to with my own game engine: gary.herostrat.us.

Enough about that though. Suffice to say that I spent a lot of time figuring out input, movement, text animations and overengineering.

Defold version so far

So far I’ve reimplemented map rendering, walking around, going from room to room.

Here’s what I’ve got so far:

http://janitor-bear-84481.netlify.com/

It’s been mostly smooth sailing so far.

Here’s some dir structure

/managers
  input_manager.lua <-- consumes input events and provides input state
                     -- (e.g. larr is held down)
  position_manager.lua <-- manages the player's place in the world and what it's
                        -- doing (e.g. walking/standing/teleporting/). That's what
                        -- the state machine include below is for.
/player
  -- ... yadda yadda yadda sprites etc
  player.script <-- gives input events to the input_manager, uses the input state to
                 -- send movement events to the position manager, and consults the
                 -- position manager to physically position the sprite on screen with
                 -- the right animations etc.
                 -- 
                 -- Bit of a god object at the moment. Too much responsibility. It
                 -- also swaps maps in and out :/ Ideally it might be a thin layer of
                 -- rendering logic delegating to gameworld logic modules. Maybe
/main
  main.script <-- loads initial level, gets camera focus, applies tints
  main.render_script <-- mostly magic I don't understand, but I did need to edit
                      -- some of this to make the window square
  main.collection <-- thanks to someone on this forum I knew to make the camera
                   -- a child of the player, in order to get the camera to follow
                   -- the player around.
  constants.lua <-- stuff like tint vectors, etc.
/levels
  /level_1
    level_1_bedroom_tiles.png
    level_1_bedroom_tiles.tilesource
    level_1_bedroom.go <-- sort of pointless atm
    level_1_bedroom_tilemap.png <-- I needed to be able to work out what was
                                 -- furniture or not, so made two layers — walkable
                                 -- and unwalkable. To see if a movement is possible
                                 -- the player sees if there is a walkable tile at
                                 -- the square the player is trying to walk to.
                                 --
                                 -- Feels a bit weird, but collision detection was
                                 -- arduous — maybe I'm doing it wrong.
    -- and level_1_lounge_*.*
    level_1.lua <-- keeps track of what map we're on, for some reason. also stores
                 -- the location of portals in each room and where they go to.
                 -- Tried this with collision detection too but couldn't find
                 -- anything that worked well.
/vendor
  moses.lua <-- functional toolkit
  statemachine.lua <-- lua-state-machine by kyleconroy
  timer.lua <-- for setting timeouts easily

Collisions

I think I’ll go into more detail in future on the collisions + portals + stuff because I’m not really sure why I should be working around it. Possibly Defold isn’t primarily aimed at RPGs so the collision stuff isn’t so applicable?

Map metadata

In any case, coming from using Tiled for maps I was really missing having my maps marked up with all the metadata I wanted and just querying them. Portals, for instance — in tiled/my engine I’d just see if I was walking on/to a portal, if so get its targetMap and targetPosition properties and follow them. I’m not sure if I’ve missed something but I couldn’t see anything really handy to do that. I suppose I could create game object as a sprite and message it for stuff but that would be hard work.

State

I’m also noticing my state is spread around really arbitrarily and my responsibility isn’t much better. I guess maybe I got lazy using Redux’s state atom. But maybe there’s something I can take from that. Defold + Lua seem really friendly towards a Redux-style state storage. We’ll see if I get bored enough to try it out.

One thing that tripped me up for a while in my old game engine was state-machine style state. I was getting really het up for a while working out, for instance, when you should be able to move:

  1. Not when you’re already moving between cells
  2. Not when you’re in the process of teleporting
  3. Not when you’re reading a text

So I’d have all the conditionals lying about everywhere guarding for that and many other situations. Eventually I read up on game programming patterns and read this really great page on a really great site: http://gameprogrammingpatterns.com/state.html

And then came up with something which, when translated to lua-state-machine, looks like this:

local _state = machine.create({
    initial = "standing",
    events = {
        { name = "walk",     from = "standing",              to = "walking" },
        { name = "halt",     from = "walking",               to = "standing" },
        { name = "teleport", from = {"walking", "standing"}, to = "teleporting" },
        { name = "appear",   from = "teleporting",           to = "standing" }
    }
})

And you can see it in use (vaguely):

function on_input(self, action_id, action)
    accept_input(action_id, action) -- Tell InputManager what we got
    if state():cannot('walk') then return end -- Can't walk? no action
    if has_movement_intent() then
        -- not currently walking, but the user intends movement, so:
        respond_to_movement_intent()
    end
    -- update walking animation. maybe this needs to be below actually?
    update_player_animation()
end

function respond_to_movement_intent(action_id, action)
    local movement_intent = get_movement_intent() -- e.g. {0, 1} is north
    local player_pos = get_player_position() -- in tiles, e.g. {1,2}
    -- Even if we can't move, we turn the player around to where they want to go
    set_orientation(movement_orientation(movement_intent))
    if movement_is_legal(player_pos, movement_intent) then
        -- Check to see if tile in user's path has a tile on 'walkable' layer
        -- Prob should be: 
        --   position_is_legal(position_after_movement(player_pos, movement_intent)) 
        state():walk(movement_intent)
        -- ^^ Change state to walk. A listener grabs our movement_intent and actions
        animate_movement(function() -- animates it, calls fn when done
            state():halt()
            -- ^^ we've halted. if user holds key down this gets picked up again
            -- in on_input — JS was never fast enough for this convenience!
            update_player_animation()
        end)
    elseif is_portal_at(position_after_movement(player_pos, movement_intent)) then
        -- Walking into a wall? Is the wall a portal? If so, follow it.
        -- Really this is a lie since portals can be in walkable squares, but it does
        -- for now.
        follow_portal(portal_at(position_after_movement(player_pos, movement_intent)))
        -- This goes off and does a bunch of complicated screen transition animation
        -- Click the link to enjoy. It took me an age. What can I say I'm dedicated
        -- to trivia.
    end
end

Edit: noticed that I could simplify a conditional in my code — there’s no state transition from walking -> walking so we don’t need to check for that separately. neat!

Conclusion

I think that’s quite enough for now! I’m sure no one is reading but it’s been very useful for me to talk this all through and I think I know what the code is calling out for (state & responsibility refactors). Apart from that, my next steps are:

  • Add the text dialogue windows and put in all my text!
  • Add the level 2 map
  • (By necessity, make following portals north & south work properly — they have slightly different behaviour to east and west)

Fun reimplementing all this Pokémon gen 1 behaviour that I’m dead certain was all wild/incredible memory hacks. I remember spending ages getting keyboard input to feel right and then realised keyboards are very different to keypads in that you can press more than one at once.

Oh, the full code if you’re interested: https://github.com/neoeno/garys_lament/tree/defold_rewrite

Anyway, that’s it for now! Thanks!!!

14 Likes

Very nice, looking forward to more of the same :slight_smile:

2 Likes

Very interesting! Thank you for sharing. I’m really looking forward to following your progress.

One things that came to mind when it comes to the dialogue system of games such as Pokemon was this recent article on it from a Lua perspective and how coroutines could be leveraged to simplify dialogue: http://lua.space/gamedev/using-lua-coroutines-to-create-rpg

3 Likes

I’m excited about your project as I’m a js developer as well and would love to hear how you progress from a web developer’s perspective. Also, I learned about Netlify which is also awesome :slight_smile:

1 Like

Hi folks!! was so glad to read your comments! they even helped me through a dark week where I started to wonder if defold was right for RPGs at all, but I’ve struggled through and I got some proper time to spend on the project this week. Have made some really great progress today.

first, the good stuff: http://masseur-square-72034.netlify.com/

that’s the first level at feature parity I think! I haven’t tested it much but I’m happy it’s mostly working. I had to solve a few problems to get there though so here they are:

Maps

This was what really discouraged me. In my JS implementation I’d written an importer for the Tiled JSON output and was just munging the data structures about to get what I wanted.

I wanted to do this idiomatically with Defold so I ditched Tiled and tried to work it out with the tilemaps and objects and such. Long story short I eventually realised that these aren’t well suited for the tiled RPG game I’m making — more for King-style physics-y object-y stuff. The biggest problem was realising this, after I decided I’d just port my Tiled stuff it got easy.

So now I’ve got:

  • My maps as static PNG images (not tiles), loaded as sprites
  • The Lua export of Tiled maps
  • A swiss-army-knife of functions I use for things like “is there a wall on the tile I’m facing?”. The hard work was already done here in my JS codebase.

For example:

M.position_is_walkable = function(map, pos)
  -- Does the collection of Room objects in Tiled
  -- include an object extending to the cell we're
  -- querying?
  local is_in_room = _.include(M.get_objects_by_type(map, "Room"), function(object)
    return M.object_covers(object, pos)
  end

  -- And are all the furniture objects not covering
  -- the cell we're querying?
  local is_not_on_furniture = _.all(M.get_objects_by_type(map, "Furniture"), function(n, object)
    return not M.object_covers(object, pos)
  end

  return is_in_room and is_not_on_furniture
end

M.object_covers = function(object, pos)
  -- Only works for rectangular objects
  local px_pos = M.tile_pos_to_px_pos(pos)
  return (
    (px_pos.x >= object.x) and
    (px_pos.x < (object.x + object.width)) and
    (px_pos.y >= object.y) and
    (px_pos.y < (object.y + object.height))
  )
end

I’m realising that the functional toolbelt I’m using (Moses) has a pretty messy API. I was searching for stuff like none, some, and even include with predicates. These exist in Moses but some take predicate functions, some take properties, some return booleans, some the key of the field, some the field itself, some take predicate functions but it’s not documented…

So I’ll be moving on at some point. I did take a look at luafun but I seem to recall having non-specific trouble with it and Defold. I’ll try it again. Short of that I’ll write my own…

The maps themselves look like this in Tiled:

I had a neat idea after a while of wrestling with converting coordinate spaces between Defold and Tiled (Defold increments from south-west to north-east, Tiled increments from north-west to south-east). I just moved all my maps below the x-axis like this:

Because then my coordinate transformation is just: { x = x, y = -y } :slight_smile: Well there’s actually a few off-by-ones but I mostly got to avoid those. And then I had to make the up arrow decrement the user’s y position instead of increment and vice versa… but really it was much simpler!

It’s a shame to be doing much of this myself rather than leveraging the framework, and it did give me some pause as to whether I was going down the right lines — but I think even with this Defold is still doing quite a lot of the work. Just not the maps stuff.

Text

I took @britzl’s advice and took a look at Lua’s coroutines — pretty handy! And I used them liberally in my text code. I’m not too ashamed to admit that I lost many 5 minute chunks to having misspelt yield in various places. I worked with Ruby for years and I still make that mistake all the time…

Each ‘Talker’ object in Tiled (confusingly named an engagement in the lua code — will be refactoring this) has an attribute specifying which text fn it should call upon to do its talking for it. I did keep all this information in the Tiled map for a while but it became very unweildy, so in the JS version I stored them in JSON and here they’re going straight in the Lua, e.g.

-- from some_texts.lua
M.window = Text.make_simple_sequence_text({
  "It's January, but the flowers are already blooming.",
  "You think some of the new shoots might be daffodils, but you've not lived here long enough to know for sure.",
  "You hope they will be OK. They shouldn't be out. The earth is changing."
})
-- from Text.lua:
M.make_simple_sequence_text = function (lines)
    return function ()
        return coroutine.create(function ()
            for n, line in pairs(lines) do
                coroutine.yield(line)
            end
        end)
    end
end
-- so then, e.g.:

local text_to_read = "window"

local text_routine = current_map_texts()[text_to_read]()

while coroutine.status(text_routine) == "suspended" do
  print(coroutine.resume(text_routine))
end

-- And we get:
--   It's January, but the flowers are already blooming.
--   You think some of the new shoots might be daffodils, but you've not lived here long enough to know for sure
--   [...]

You can probably see how this would be extended in future to allow conditionals etc.

Text animation

This was a tricky one. Here’s the rules:

  • Hit return next to an item and the text starts typewriter-animating, e.g. 1 letter at a time
  • If the user hits return again before the text has finished animating, it jumps to the end of the animation
  • If the user hits return after the text has finished animating:
    • If there is another section, it animates that
    • Otherwise, it closes the dialogue

Which I operationalised to a state machine like this:

machine.create({
  initial = "stopped",
  events = {
    -- Start animating a new section
    { name = "animate",  from = {"showing", "stopped"},   to = "animating" },
    -- Complete the animation naturally
    { name = "complete", from = "animating",              to = "showing" },
    -- Skip the rest of the animation
    { name = "skip",     from = "animating",              to = "showing" },
    -- Close the dialogue panel
    { name = "finish",   from = "showing",                to = "stopped" },
  },
})

Not totally happy with those names but they’ll do for now. I’ve set the state machine to post a message to the GUI script whenever it changes state — which is a nice flow that I think I’ll use again in future.

-- main script

-- This is simplified
function respond_to_engagement_intent()
  -- e.g. if the user has hit enter
  if not current_text_routine then
    -- [...] start a new text routine
  end
  if text_machine():is("animating") then
    -- if we're currently animating, skip to the end of the animation
    text_machine():skip()
  else
    -- Get our next text
    -- Shame `status` isn't more useful to us here...
    local status, text = coroutine.resume(current_text_routine)
    if coroutine.status(current_text_routine) == "suspended" then
      -- Presuming the coroutine hasn't finished yet, animate the text
      -- maybe there's a way of avoiding this conditional if we organised
      -- the coroutine differently... hmm
      text_machine():animate(text)
    else
      -- Otherwise, finish up
      text_machine():finish()
  end
end

-- gui script

function on_message(self, message_id, message, sender)
  if hash("text_machine_state_change") == message_id then
    if message.event == "animate" then
      gui.set_enabled(self.dialog_panel_id, true)
      initialize_text_animation(self, get_current_wrapped_text())
    elseif message.event == "skip" or message.event == "complete" then
      self.text_animation = nil
      gui.set_text(self.dialog_text_id, get_current_wrapped_text())
    elseif message.event == "finish" then
      gui.set_enabled(self.dialog_panel_id, false)
    end
  end
end

Then I used another coroutine to manage the typewriter animation:

function initialize_text_animation(self, text)
    self.text_animation = coroutine.create(function()
        for i = 1, string.len(text) do
            gui.set_text(self.dialog_text_id, string.sub(text, 0, i))
            coroutine.yield()
        end
    end)
    step_text_animation(self)
end

function step_text_animation(self)
    if not self.text_animation then return end
    if coroutine.status(self.text_animation) == "suspended" then
        coroutine.resume(self.text_animation)
        timer.frames(timers, 2, function() step_text_animation(self) end)
    else
        text_machine():complete()
    end
end

Why not just recursively call the timer here and be done with the coroutine? Good question. It would have been plausible to do it that way. I’m not sure if it would have been cleaner but it might have been.

Note: I got bit by a shared-contexts gotcha when using @britzl’s timer module (thanks btw!). If you just call timer.update in one on_update in your main script and just use the timer scheduling elsewhere you’re all cool — until you decide you want to schedule something in a GUI script. Then the contexts get confused and you get an unusual error.

I resolved this by making the timer stateless and making the caller responsible for storing the table of timeouts. So now instead of:

function on_update(self, dt)
  timer.update(dt)
  timer.frames(1, function() print("one frame later") end)
end

It’s:

function init(self)
  self.timers = {}
end

function on_update(self, dt)
  timer.update(self.timers, dt)
  timer.frames(self.timers, 1, function() print("one frame later") end)
end

That way the context is always going to be correct, as long as you don’t do anything weird. Note that in fact I didn’t put timers on self but instead made it a local as all my scripts so far are singletons… I’m sure I’ll end up paying for that later.

Next steps & miscellanea

I’ve still got a few refactors I want to make. Mostly just clean-up and name changes from today’s work — and maybe it’s time to remove some concepts that aren’t proving their worth… but more functionally:

  1. Get north/south portals working
  2. Add the second level
  3. Add some sort of level selector (maybe, or just make them separate projects or whatever)

I also spent some time trying to figure out how to make a redux-style state store work in Lua/Defold. There are some nice features of Lua that make it a compelling idea but ultimately I got stuck on the same thing that tripped me up in JS: how to deal with stuff-in-progress. E.g. when I’m walking from one tile to another, what should the store be reflecting and how can we bounce control back and forth between the store and the UI code without making it an unholy mess. I spiked out using coroutines there too but it was a real nightmare.

Seems like in games it’s difficult to cleanly separate async stuff like animation from the ‘business logic’. Possibly it’s not even worth trying and it’s my web dev brain making me think it’s an issue when it’s not :slight_smile: I’m sure there’s some way of making it work nicely, maybe even a good paradigm in there, but it’s not what I’m here for so I rolled it all back.

Anyway — thanks for reading if you are! Or thanks for skimming ahaha. See you next time!!

8 Likes

I’m happy to hear that you haven’t given up on Defold! I think Defold will work very well with the kind of game you are doing, but as you have already discovered there is not always a 1:1 mapping of how things are done in other engines. In Defold some things are done slightly differently and it’s important to not try to work against the engine. We should perhaps consider adding a few “migration” guides for devs coming from other engines like Unity, Game Maker, Phaser etc

Some other thoughts:

“My maps as static PNG images (not tiles), loaded as sprites” - I think you should try to create Defold tilemaps at run-time based on the map data from Tiled. You can achieve this by using the tilemap.set_tile() function. My colleague @Andreas_Jirenius is showing this in one of his video tutorials: https://www.livecoding.tv/setthebet/videos/yL3yE-shmup-game-integrate-tiled-in-defold-4

“Seems like in games it’s difficult to cleanly separate async stuff like animation from the ‘business logic’” - Yes, this is for the most part true. Depending on the game it may or may not be worth it. Mostly it’s not.

“Defold increments from south-west to north-east, Tiled increments from north-west to south-east” - This is a constant source of confusion. I belong to the camp of developer who would have preferred top-left to bottom-right, but I’ve mostly become used to the other direction now. The historical reasons for “top-left to bottom-right” vs “bottom-left to top-right” can be read about here: http://rampantgames.com/blog/?p=10414

I wish you continued good luck on the game and look forward to the next update!

7 Likes