Wow, it's been almost a year since I last updated this blog.
I haven't had time to work on Fugue until a month or so ago. Since then I've been chipping away at more tooling. Once the core game mechanics/systems are in place, I'm expecting that most of the time will be spent creating content for the game: writing, modeling, environment design, etc. So I'm building out tooling and figuring out strategies to streamline all of these processes.
Godot makes it easy to develop editor plugins that integrate relatively seamlessly. It's not without its challenges and frustrations but those are more to do with Godot in general than specifically about their plugin development process (see below).
The game will play out mostly through characters saying and doing things, and these actions need to be specified in a way where I don't need to meticulously program each one. Previously the game's narrative elements used "Dialogue" as the main organizing element, focusing on spoken dialogue, and let me write "scripts" of spoken dialogue lines with a playback system to have the appropriate characters say their lines in order. That ended up being too limiting because I want to write not only dialogue but to specify various actions/stage directions to write scripts that basically describe entire scenes, including character movement and animation, sound and environmental cues, and so on. So I restructured that whole system around "Sequence" as the main organizing element, with "Dialogue" as a sub-component.
A Sequence is composed of "Actions", which include dialogue lines, choice prompts, animation triggers, game variable setting, character movement and rotation, etc. At the time of writing the following actions are available:
L): A line of dialogue, spoken by a single Actor.
%): A set of choices that the player must choose from.
V): A line of voice-over dialogue. The difference between this and
Lineis that it does not require the speaking actor to be present and shows in a fixed position on screen.
?): Basically a combination of
Decision. A line of dialogue is displayed with the decision's choices.
#): A blocking pause in the sequence
=): Set a state variable to the specified value (strings only). There are a number of targets
Global: Set it on the global state
Sequence: Set it on the local state (local to the current sequence). These values persist through multiple executions of the same sequence (i.e. they aren't reset whenever the sequence is run again).
Actor: Set it on the local state (local to a specific actor).
>): Play an animation with the specified for the specified Actor
->): Move the Actor to the specified target
- You can use this to "bounce" the player character if they enter somewhere they aren't supposed to.
@): Have the Actor look at the specified target
N): Toggle the visibility of the specified node. Can fade it in/out (but looks quite janky)
>N): Move a node to the position and rotation of the specified target. This happens instantaneously...so you could use it for teleportation; but more likely you'd use it to rearrange a scene while it's faded out.
P): Enable/disable the specified portal.
+): Add an item to the player's inventory
))): Play a sound
~): Start or end a Parable (Quest)
F): Complete fade the scene in or out
>S): Change the scene. Because sequences are associated with one scene, this will end the sequence!
Sequences may be triggered in one of three ways: the player interacting with an object (such as talking to an NPC), the player entering a zone/area, or they automatically start when a scene loads ("ambient" sequences).
A "Sequence Script" is a graph of two types of nodes: "Verses", which are lists of actions, and "Forks", which include one or more "Branches" that each have a set of conditions. If a branch's conditions are true then its child verses are executed.
Sequences are associated with only one scene. Generally multiple sequences will be related in some way: the might be part of the same narrative arc, for example. So Sequences can be further organized into "Stories" which is basically just a grouping of Sequences, without any significant additional functionality.
The Sequence and Story Editors both make it very easy to quickly sketch out scripts and later refine them. They both have built-in validators to ensure that scripts are correctly specified, i.e. they don't refer to any objects that aren't in the scene, aren't missing any required data, etc.
Sequences and Stories are just stored as relatively simple JSON so they can be further processed/analyzed outside of Godot easily.
I expect that as the game's writing and development continues more actions will be needed. But for now this set has been comprehensive enough.
Finding source images that fit my licensing requirements and then editing them into textures is a very tedious process. I built a web tool that makes it much easier to find public domain and CC source images (vastly simplified by Openverse), cut out clippings from them and pack those clippings into textures or generate seamless textures by wrapping and blending their edges. It tracks where the clips were sourced from so that attribution is much easier to manage.
I'm not at the point where I've given a ton of thought to the game's music, but I do have one tool,
dust, that I developed to help sketch out musical ideas. I didn't develop it specifically for this game but it'll be useful here too. It's a chord progression generator/jammer that outputs MIDI so it can be used as an input to most DAWs (tested with Bitwig Studio and Live 11). It helps to get around blank-canvas-syndrome by giving you a chord base to start working with.
I've started working on the item system, which is very simple at the moment (and hopefully will stay that way). To manage the items I created an Item Editor, which, though a lot simpler than the Sequence Editor, is just as useful.
Blender scripts and templates
Blender's also been nice to work with because of its support for Python scripts. It's a little clunky to get things integrated, but can be powerful once you're going. In my case I'm mostly using a "quick export" script that helps me avoid the tedious work of keeping exported files organized (navigating to the correct folder, setting the filename, etc) and double-checking my export settings are correct. In the case of items, which require a static icon to show in the UI, the export script automatically exports a properly-cropped render of the item to the item icons folder so I don't have to bother with that at all.
Another small but immensely helpful thing is having a specific template for Fugue modeling work, with materials, cameras, and what not preconfigured. My material settings change very infrequently; I'm usually just swapping out textures, so this saves me a lot of time configuring materials over and over again.
Dialogue Layout System
Not really a tool, but something I've been refining for awhile now. This is the system that determines where dialogue boxes are placed on screen. Many games have a fixed dialogue box, e.g. at the center bottom of the screen, but I want it to feel more spatial, especially as there won't be any voiced lines in the game (too expensive/too much work/difficult to change and iterate on) so there won't be any 3d audio to offer that auditory depth.
As far as I know there is no easy or reliable way to layout rectangles in a 2d space to guarantee that there are no overlaps. Not only should there be no overlaps, but each dialogue box should be reasonably close to its "host" (the actor that's speaking) so that it's clear who's speaking. I have a reasonable expectation/constraint for myself that something like five at most actors should be speaking at once and the game has a minimum viewport size to ensure there's a reasonable amount of space. That is, I'm not expecting that overlaps will be impossible, only that they are unlikely given these constraints.
The approach I'm using now is using a fixed set of anchors for each object and a quadtree to detect collisions. We just try placing a box at one of an object's anchors, and if it collides with an existing dialogue box or object, try the next anchor.
As you can see from the prototype above (on-screen objects are beige, off-screen objects are grey, and dialogue boxes are black), it's not perfect. The box
8 at the top overlaps a bit with object
2 — this is due to how dialogue boxes for off-screen objects are handled, which could be refined, but I'm treating as an acceptable edge case for now.
Another shortcoming is how 2d bounding boxes are calculated from 3d objects. Basically I compute the bounding rectangular prism around the object and project that to 2d space. Depending on the shape of the object that may work well or it may end up placing an anchor far-ish from the object's mesh. You can kind of see it in the screenshot below, the "I'm on the move" dialogue box is meant to accompany the smaller NPC, but it's kind of far away. Tighter bounding boxes might be possible but I'm worried about the overhead they'd require. Something to look more into.
Godot doesn't have its own unit testing framework but there are two popular third-party options:
Gut. They both seem fairly powerful but
Gut felt a bit clunky and I couldn't get
gdUnit3 to work properly (compile errors, which I chalk up to Godot's weird stochastic-feeling nature, more on that below). I ended up writing my own very simple testing framework instead. It lacks basically all of the advanced features present in other testing frameworks (spies, mocks, etc), but for my needs it's working great.
Things not covered here
There are still a few key content areas that I don't have a good approach for:
- Character animation. This is something I'm not very good at and a huge factor in the visual quality of the game. Crunchy textures and low-poly models are a lot more forgiving than terrible animations. There are now deep learning motion capture tools that might work with a commodity web camera, but I haven't tried them yet so I don't know if their output is good and what the workflow is like.
- Mapping textures. Taking a texture and then adjusting a model's UV map so that it doesn't look warped is also really, really tedious. No idea how to streamline that.
- Object modeling. This is harder to streamline/automate because there's so much variation. Some categories like buildings and plants could be streamlined through procedural generation via Blender's geometry node system. Fortunately I enjoy modeling so I don't really mind doing this, it'll just be very time consuming. One more general possibility is to figure out a decent processing pipeline for taking free models and converting them into an polygon count that matches everything else. But finding an approach that is robust enough seems unlikely.
- Character modeling. To make the world feel lively I'd like to have many, many background characters and a fair amount of more important NPCs. This might be doable with some kind of procedural/parameter character variation system (i.e. creating a few key archetype models, then having a script to vary some vertices, scales, etc) alongside with a procedural texture generation system (for clothing, etc). Again, this might be doable with Blender's geometry node system.
Thoughts on working with Godot
I've spent a far amount of time with Godot while working on all of this. My only reference point is Unity, which was very unpleasant to work with. Everything felt so fragile. Small changes can break tons of other things, with no easy way to undo the damage. Kind of like how in Microsoft Word adding a space can mess up your whole document's layout.
Godot has overall felt better than this, but it still has a similar, if reduced, fragility. I've felt discouraged to experiment with new ideas out of the fear that I will just break a bunch of existing code/scenes and have to manually fix everything. I've found that even just opening a scene file can alter its contents—not yet in a way that has caused me trouble, but it's still very different than other programming work I've done, where things really only change if you change them. It's like trying to build a house on moving ground. Version control is a bit of a safety blanket but its effectiveness depends on what changes I can revert to.
GDScript has been a surprisingly pleasant language. It still lacks many features I'd like like sets, first class functions, and iterators (all of which I believe are coming in Godot 4) but usually those haven't been an issue. What has been very frustrating is how Godot parses/compiles scripts. If you have a syntax error in one file it ends up breaking a bunch of other files that depend on it (which is to be expected) but it reports these issues as obscure errors that don't point to the originating error. I'll be inundated with messages like
The class "YourClass" couldn't be fully loaded (script error or cyclic dependency). (it will not point you to what this error might be) or mysterious errors like
Cannot get class '_'. repeating several times. Then it requires a painful process of trying to figure out where the syntax error actually is by opening scripts one-by-one until I stumble upon it.
This is less likely to happen if you're using Godot's built-in script editor because you're more likely to catch the syntax error before it causes too much trouble. However Godot's built-in editor is really lacking, mainly because you can only have one script open at a time and if you need to edit multiple files at once it requires a very tedious process of manually jumping between files one at a time. So I use an external editor, which does have a language server integration with Godot—so it does catch syntax errors, but sometimes I don't catch them in time, and then these dizzying cascading errors happen.
I've also noticed that sometimes there will be compile errors that are fixed by reloading the project. It feels like these happen because of an unusual (sometimes seemingly random) parse order for scripts, like classes are found as undeclared when in fact they are. I haven't looked into it too much.
That all being said, Godot has been wonderful to work with overall. These frustrating experiences are infrequent, and it sounds like many of them are being addressed in Godot 4. I've enjoyed it way more than Unity and it's an amazing privilege to have access to such a powerful open-source project!