Uncovering some of the technologies behind High Priestess in Rags
My latest visual novel makes use of a brand new engine, with some unique properties. Let’s look at some details of this engine.
- written in modern C++, using common games industry practices
- specialized scripting format, incorporating Lua scripting
- mixed 2d and 3d capabilities (even though this game only brushes the surface)
- sophisticated 2d layout capabilities
Why a custom engine?
The honest answer was that it was easier for me this way. But that was only because I had a lot of these technologies prepared before. I’m an experienced enough developer that it’s convenient to work in a very flexible environment, and develop what I need when I need it.
This approach allowed for many things that would be difficult in many tradition engines. For example:
- the page flip transition requires freezing the narrative state at a particular point, then rendering it to and offscreen texture, and then projected that onto a procedurally animated mesh (if you look closely, you may notice that both pages can animate during the flip)
- compute shader based fractal and particle simulations, such as the interactive “lithelike” logo at the start
- 2d and 3d elements interleaved, such as the candles on the chapter screen
- the word-by-word fade-in effect, combined with justification settings, which required a very flexible text rendering engine
Furthermore, the code that places 2d elements on the screen (ie, the layout engine) is more featureful than most. This allowed many advantages:
- the narrative box adapt intelligently to screen size and font size (even behaving well when a full text prompt cannot fit)
- when presenting narrative choices, the boxes are arranged intelligently based on both text size and string length, even for long messages
- the options dialogs look and act like modern UI elements with limited modality and no pop-ups
These kinds of things have an impact on the overall user experience (though sometimes second-level). Despite their importance, they can be difficult to achieve without a very flexible engine.
However, the most important advantage for me was the ability to expand this technology into game prototypes of different genres. Without using a custom solution, it’s not straight-forward to combine this style of storytelling into an interactive simulation in another genre.
Using two separate engines within the one project is not very viable. And attempting to expand one genre-specific engine to incorporate the functionality for another genre is also typically a very problematic process.
Advantages of high performance languages
C++, though more difficult to use, has several advantages for the end product. For one, you get the lowest-level and most flexible access to the underlying graphics API -- allowing the developer to achieve very particular results.
Of course, performance is also a benefit. C++ allows very specific control over the code that is generated, control over memory layouts, and usually a significant reduction in the number of architectural layers and complexity involved in digging down to OS and hardware features.
Here, it also simplified installation. High Priestess in Rags is entirely self contained in a single, portable executable file. There is no complex dependency chain of libraries and language related requirements. This is too frequently not considered when using high-level languages, but this project shows it’s possible to boil everything down to the most minimal end result.
Disadvantages
Of course, there are disadvantages to this kind of approach. Most notably, broad hardware support is far more expensive and difficult. When combined with the low-level Vulkan API, this can get especially difficult. There really are no safety barriers. Worst of all, there are problems -- such as buggy drivers and buggy overlay software -- that can cause even correct code to have compatibility problems.
With a higher level engine, most platform support issues have already been worked out. But, since I use the Win32 API and Vulkan APIs directly, I have to handle all of this myself. It’s particularly difficult since I only have access to a finite amount of hardware to test with.
Multi-platform support (ie, other OSs and hardware, such as OSX or ARM) are a bit of a win/lose scenario. On one hand, I have to build that support myself. But I can build it in such a way that each platform is basically a first-class citizen in the tech infrastructure. Many large engines don’t actually work this way. They specialize in a particular platform/tech environment and then use compatibility layers to adapt to other platforms. These compatibility layers tend to be very poor and problematic.
Another thing to consider is that there’s a heavier load in terms of developer expertise. The more low level an API is, the more demanding it is on the developer using it.
Custom scripting
Given that there are so many lines of text in this project, I wanted to be sure that I could write the script in the most “intentional” manner. By that, I mean the script should reflect my intentions clearly, with little extra scuff.
Here’s an example of a typical line:
beatrice| “Oh, don’t intimidate [h-o] so, darling,” said Beatrice. “You’ll put [h-o] right off. Anyway, I think [h-p]’s soon concluding [h-as] business.” |
---|
This line identifies the speaker for the engine, and speech is quoted in a natural way (for automatic coloration). Text in square brackets allows me to include words that adapt. In the example, these are pronouns related to ‘h’ (who is the player character).
Lua fragments can be embedded between lines of dialogue. For example, here we build and present a “choices” structure, which is used for presenting options to the player.
```
local choices1a = beginChoices(“jemma-questions-1a”) choices1a:add(“Yes”) choices1a:add(“No”) return ShowNarrationLayer:new(1, choices1a) ``` The first question: “Are the attacks on travelers and the automatic veneration related?” |
---|
Lua is a convenient option, mostly because of its straight-forward integration with a C++ based host. There’s very low overhead in going across the barrier between Lua and C++.
Also, recent versions of Lua support coroutine style behavior. I haven’t tried this yet, but I mean to. The nature of coroutines makes them quite convenient for the way in which we want the C++ and Lua code to interact. Coroutines allow the host to start and stop the script between lines and code fragments — which is natural when you think about how we want the script to work.
It’s important when using scripting languages to balance what functionality is on the scripting side vs the native code side. The project only required a very limited interface between the script code and the C++ code and yet could create a very rich experience.
Follow-ups
If you’re interested in hearing more about specific elements of the technology in this work, let me know.
There are some curious details behind some of the procedural effects, and 2d layouts, as well as the architectural abstractions behind the game structure, bookmarks, etc.
Get High Priestess in Rags
High Priestess in Rags
You don't have to be a hero to have a heroic tale.
Status | In development |
Author | Lithelike |
Genre | Visual Novel |
Tags | Fantasy, Narrative, Period Piece, Story Rich |
Leave a comment
Log in with itch.io to leave a comment.