A Fancy Textbox Part 1: DSL
In my Ludum Dare 53 postmortem I wrote a bit about creating a textbox component, and mentioned I wanted to revisit and expand on it in the future. Well I did! In this series of blog posts I'll walk through each part, starting today with the DSL. I'll be doing everything in Janet Lang, with the expectation of running within TIC-80. However much here could be applied to other environments.
As a tease, this is what we'll be building towards.
Syntax
The first step is to decide on a DSL (domain specific language) for the text within the textbox. I had some basic desires for this starting out. It should be a single string, it would need to be able to handle formatting commands (such as color, text speed, etc), and it should include meta info about the context of the text (for now just the name of the character talking). Searching around the internet I found a couple really good references and inspiration
- The Speechless Standard List library by Shirakumo, created for their game Kandria. I drew most of the actual syntax from this library, which itself is based on markless. My textbox will not support markless, but will look a bit like markless "quotes".
- this wonderful twitter thread by Alec Faulkner on Mina the Hollower's font had a lot of great practical advice about how to display the text within the textbox. The main takeaway for me was to let the author manage splitting the text across multiple panels. In his words "You can always split a sentence between multiple text boxes to break things up, but that’s not always ideal, since it can interrupt the flow of conversation", flow is important!
- Yarnspinner is a popular "tool for writing game dialogue" used in actual real games. Though I didn't draw a lot of specific stuff directly from it, I'm guessing I'll steal how they manage branching dialogue... whenever I get to that
So here's the DSL we'll be using.
~ Character Name | Each 'bar' represents a new panel to display. Newlines are optional but help when writing. | (voice 2) The voice number is the sfx id to play on every character. | (color 4) The number 4 is red in tic80 (color 12), and 12 is white. | (speed 0.1) Speed will change the time between character progression, (speed 1) and can be any positive number. | (wiggle true) wiggle will make the text bounce up and down, how fun!
Note that formatting commands are the things that look like lisp functions, (command-name arg), and can be placed anywhere after the character name. The 3 formatting commands we'll implement here are voice, color, speed, and wiggle. Though more could be easily added! We'll consider newlines optional, but preferred for readability.
Parsing
Now that we have a text format, we need to convert it into something more convenient to work with. The trick here is to use Janets Parsing Expression Grammars to parse our DSL into a convenient data structure. I won't go into detail about how PEGs works here, instead I'll just paste the entire parsing function all at once. Know that PEG's are amazing and better then REGEX in every conceivable way.
(defn parse-script [input]
(peg/match
~{:bool (+ (* (constant true) "true")
(* (constant false) "false"))
:pos-int (number (some (range "09")))
:pos-float (number (some (+ (range "09") ".")))
:command
(replace
(* "("
(+ (* (constant :voice) "voice " :pos-int)
(* (constant :speed) "speed " :pos-float)
(* (constant :color) "color " :pos-int)
(* (constant :wiggle) "wiggle " :bool))
")" (any " "))
,struct)
:word
(replace
(* (constant :word)
(<- (* (some (+ (range "az" "AZ" "09") (set "',.?!"))) (any " "))))
,struct)
:character-name
(replace
(* (constant :name)
(replace (* (<- (some (+ (range "az" "AZ" "09") " "))))
,string/trim))
,struct)
:source-line
(* "~ " :character-name (any :command))
:dialogue-line
(* "| "
(some (choice :command :word))
(constant :wait))
:main
(* :source-line
(some :dialogue-line))}
input))
Lets run this function over some simplified text in our DSL and see what we get.
(pp (parse-script "~ Character Name
| (color 1) hello world!
| pizza hotdog"))
# Output formatted for your reading pleasure
@[{:name "Character Name"}
{:color 1}
{:word "hello "}
{:word "world"}
:wait
{:word "pizza "}
{:word "hotdog"}
:wait]
Hopefully you can see what happened here. Our DSL is translated to a sequence of objects, containing the type of data (command or text) and the data. Let's call each one of these items a Script Command.
Note the wait command which is added in between every panel text. I chose that word because in-game we'll be awaiting player input to progress to the next panel.
Perhaps the most unintuitive thing here is that every word becomes a unique script command.. Why not combine all the text together into a single string? Well, remember that commands can go anywhere. Keeping each word separate simplifies how we'll process script commands later on. It will also simplify word wrapping when we get to actually drawing stuff in TIC-80. Basically, trust me on this one.
I had originally planned to write a single post about everything in the textbox.. but it just got too long. So tune in next time for when we work through the internal logic of actually using our script commands!