I skipped last week because it was the beginning of a vacation week for me, so here I am! I mostly worked on some art-related things for The Five Words of Power, this week.

Checklist

  • Make the Alice is Dead conversation parser I talked about briefly last time more flexible
  • Tighten up my offset tile layer work (that I also talked about last time)
  • Figure out how I’m dealing with walls and cliffs in The Five Words of Power

Conversation Parsing

A couple years ago I wrote about the cutscene system I created for Black Mountain. Similar ideas form the basis of what I am building in Alice is Dead for scripting conversations, but in this case I’m defining a branching series of “pages” in a conversation. The player gets to respond at each page, which sends them to another “page” of conversation. (Internally I call them “nodes” because really what I’m describing is more properly a directed graph, but it’s a lot easier to explain like a Choose-Your-Own-Adventure book, which is why I like the term “page” when I’m talking about it, especially since the term Node is so overloaded in Godot.)

This was all working before, but for a later part of the game I wanted to be able to change what options were available based on game flags – if you’d already done something you couldn’t ask about it anymore, or if you had an item you could offer it in the conversation, that sort of thing. I had intended to add those sort of things to the system eventually, so … it was time! Unfortunately I realized that adding these would mean kind of hacking them into the system, because I’d been focused on getting it written quickly, and to do a specific set of things, before.

Specifically, I had this sort of thing working:

$ who_are_you
What a rude thing to say! Don't you know you should introduce
yourself before you ask someone who they are?

    [ Fine. I don't remember who I am. ] dont_remember
    [ What are you, my etiquette tutor? ] etiquette_tutor
    [ Just get out of my way. ] out_of_my_way

This defines one “page” in the conversation, and gives it the name who_are_you; this is like the “page number”, basically. The bare line(s) after that define the text that the conversation shows next, and then the lines with the [] characters define the responses. The square brackets contain the response, as displayed, and after that is what page to jump to if the player picks it. Pretty straight forward! Internally, I implemented this by breaking up my text file into separate lines, and looking at the first character (ignoring whitespace) – in this example, we have $ to denote the start of a node, and [ to denote a reply line. Then, I can split the line on white space or whatever, depending on the line, to break it up into words.

Now I wanted to add:

$ some_part_of_the_conversation
Tell me something interesting!

    [ I have a bell ] have_a_bell ? has_item(bell)
    [ I saw three birds. ] saw_three_bird ? bird_count > 2
    [ Never mind. ] never_mind

So now my reply lines were getting a lot more complicated. They might have a ? after the node name, and that might be a function call (has_item()) or it might be a flag name (bird_count; technically this maps to get_flag(bird_count) internally), and it might have a comparator operator (==, or >, etc). I could have just split on whitespace, again, or do something slightly more complex to try to split on “word boundaries”, but what if I want to be able to use quotes? There’s nothing internal to my engine that disallows whitespace in flag names, so it would be possible at some point to have bird count, so I’d need to check for a " character and then split on the next " character… and so on. It just got unwieldy and over-wrought the longer I picked at it, so I decided it was just time to take a step back and treat it a bit more like a traditional parser.

I say a “bit more” because I didn’t go the whole way into writing up a full grammar definition for my conversation scripts and writing a recursive descent parser or whatever. I still treat the input line-by-line, and even use the first (non-whitespace) character in the line to determine how to parse it. Some lines really don’t need more than “strip off the first character and store the rest of the line as a string”, so why go to the extra work there? For replies, though, I work through the line character-by-character, tokenizing the line as I go so I can be a bit more flexible and nuanced than “just chop it all up at whitespace or something”. If you’re interested in learning about tokenization, writing parsers, etc, that’s somewhat beyond the scope of this blog post – and unfortunately it’s been a long time since I did any formal learning on it, so I don’t even necessarily have any good suggestions for where to go to learn more. There are tons of resources out there – “how to write a parser” is probably a good starting point for a Google search. (Also I would suggest that “writing a parser for something in GDScript” is probably not a great way to dip your toes into it; there are much better environments to build parsers in.)

So anyway, now I have a more expressive parser for my conversations, which is nice. I’m not actually sure it was worth it, in the end; I probably could have just come up with some syntax that I could kludge together and carried on with other things rather than sinking quite a bit of time into this rabbit hole, but it’s done now, and it is “better” in a number of ways, so that’s a win, I suppose? ¯\_(ツ)_/¯

Checking My Work (And Maybe Saving It For The Future)

So the other thing I did, which is inarguably more useful, is that I finally got around to looking up how to add in build-time scripting in Godot. In this case, I pulled my conversation parser out into a plugin, and added a _build() func to it (see the Godot docs), which let me load and parse all of my conversations at build time – every time I run the game, it first checks to make sure I haven’t messed up with the syntax in one of my conversation files.

The next part of this step, which I still haven’t tackled, is to take the compiled form of my conversations (the outcome of all the parsing nonsense I described above is a structure objects with everything put into appropriate fields that the runtime conversation system uses) and save that at export time, and package that with the game. I don’t expect conversation parsing to be a huge amount of work (it seems to run quite quickly), but I’d like to see how this flow – processing things every time you run, in the editor, but process it once at export time for distributables – works in practice.

Walls

After I made that progress with Alice is Dead, and with a week away from my dayjob, I wanted to get a good leg up on figuring out how to structure the art I wanted to make for Five Words of Power. I wrote about “Tile Offset Layers” last time, and this week I wanted to tackle walls and cliffs. I knew what I didn’t want to do.

Traditionally in a 2d top-down game, you have essentially two layers: what’s drawn behind the player and monsters (what I collectively tend to call “entities”) and what you draw over them. As long as you follow a few rules, this works pretty well, but it has drawbacks: Your workflow is less than ideal, since you have to work back and forth between the two layers, drawing the bottom half and the top half separately, and this can lead to issues where bits cover up the characters’ heads when they shouldn’t, or the characters’ feet are visible on top of things that they should be behind. Thankfully, Godot has a surprisingly simple solution to this: oversize tiles and the way its y-sorting works.

Oversize tiles are just like they sound: tiles that take up more than “one tile” worth of pixels in the tile set. You can create these in your tile set on the Setup pane by holding Shift and dragging out the full size. Then, either through Select or Paint, set its texture origin so that it fits against the bottom part of the tile. This makes it a lot more reasonable to place on the map.

Y-sorting is a built in thing in Godot, that’s a simple toggle. If you turn it on for a Node (it’s under CanvasItem > Ordering > Y Sort Enabled in the inspector), all of its children will be drawn such that anything lower on the screen will be drawn in front of things that are higher on the screen.

The way that TileMapLayers are implemented also ties into the y-sort settings. First of all, if you have y-sort enabled on a tilemap, any oversize tiles will y-sort with each other. In addition, if you set a container Node to have y-sort enabled, and it contains some scenes and one or more TileMapLayers that also have y-sort enabled, everything will be y-sorted with each other – your entities and tiles will intermix properly.

Combine all of that with Godot’s auto-tiling and you can place walls in such a way that you just draw the wall with one click, without having to switch around on the layers or fuss with which bits of “upper” walls go with which bits of lower walls. I can see this is going to be a huge time saver for me, especially when it comes to things that are almost but not quite walls: cliffs.

Cliffs: The Thick, Slanty Walls

I had a particular vision of how I wanted cliffs to work. I didn’t want them to be arrow-straight up and down, but to have a steep slope to them. There are a couple of things that come from this: the back won’t be super tall so things won’t get occluded as much behind a cliff (which is always annoying), and the sides are going to have thickness to them rather than just going straight up to a top. Ideally I’d also have a way to use the top of the cliffs as another terrain spot.

Using the basic template from the walls worked… eventually. The slope means you actually have to make a sort of parallelogram-shaped tile, so they overlap properly. I actually settled on making the tiles even larger than the walls – each cliff tile is made up of a 2x3 rectangle of tiles, with the texture origin set up appropriately. This gives me the ability to overlap in every direction, including off to the sides or down onto the tile in front. This lets me have more natural looking edges, but it also means that I have to be really careful with the overlaps – y-sorting is great for up-and-down overlaps, but not so much for side-to-side.

It took me a few days to get a lot of the specifics sorted out (and at least one night I was literally dreaming about pixelling rocks on cliffs), but I’m pretty happy with the outcome.

So! Work continues on with Alice is Dead and The Five Words of Power. I’m happy in particular with how the visuals for Five Words are coming together. Still have a long road ahead of me, but it’s looking good so far. See you next week!