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:
- Not when you’re already moving between cells
- Not when you’re in the process of teleporting
- 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!!!