Narra : A declarative narrative scripting language

I was shopping around for a clean and flexible narrative scripting language for a game project, and I couldn't find anything that works. So I decided to make my own...

Introducing Narra, a declarative narrative scripting language that somehow turned out to be similar to the web in its approach. Probably because narrative scripting has very similar problems in terms of merging declarative aspects, state and dynamic elements.

Advantages of a textual narrative language as opposed to the graph approach :

  • For the writers, it's much easier to type out their stories than to fiddle around with spaghetti graphs. It's also much easier to share and collaborate.
  • In terms of source control, it's much easier to deal with non binary data. Most graph based narrative scripting tools end up saving to a binary form.
  • Extensibility : Advanced users can easily write pre processors for the language to create their own flavors of it, tooling around narra projects ...etc. Ofcourse, the textual aspect of the language also makes it really easy to make it as general as possible.
  • AI : Language models can easily pickup on Narra's syntax with few-shot learning, and I was even able to write down Narra experiences with the help of Copilot.

Model

In narra, a narrative is defined as a forest of dialogue trees, with each tree defined as a sequence of actions. By composing narrative in such a way, we can freely define recurring trees, jump between them and it generally simplifies branching. Actions are things such as "dialogue elements", choices, matches (switch statement), evals ...etc.

Additionally, you can make use of a scripting language like Lua to define the logic of your narrative. Narra compiles the dialogue forest into a JSON tree, and combines it with the Lua code into a single .nb file.

A tour of Narra

Execution starts by default at a tree with the name "main", the following example shows "Hello World !"

@main
"Hello World!"

If it's said by some character "Bob", you can just put it before the text.

@main
Bob : "Hello World!"

If the name has spaces, you can surround it with "_" :

@main
_Bob John _ : "Hello World!"

You can also use a character association table (some json file) to avoid repetition. So you just use "b" for the name for example.

As I said before, each tree is a list of actions, actions are seperated by -> which denotes a sequence.

@main
b : "Hello World!"
-> b: "Here is some other text !"
-> b: "One last bit of text"

Text can be glued together with the glue operator "<>", this is especially useful when dealing with dynamic values, which we will touch upon later on down the post :

@main
b : "Hello " <> "World!"

In order to make the language as general as possible for most projects, Narra has a "modifier system". If you think of the action tree as HTML, modifiers are the CSS of Narra.
Modifiers follow the general pattern :

modifier_name:value, where value is either a static value or a dynamic one (we will get into that part of narra later..)

Some are handled natively by the Narra engine, others are passed on to the application that uses Narra to handle.
For example, in a web visual novel context, we can directly forward modifiers into css[1] [2] (A use case that I've already tested and experimented with).

@main
#text-color:"red.600" #text-fontWeight:"extrabold" b : "Hello " <> "World!"

A second type of basic actions are choices :

@main
b : "Hello World!"
-> @choice
|"Option 1"| "Action sequence to execute when this choice is selected.."
|"Option 2"| "Action sequence to execute when this other choice is selected.."
;

To keep Narra as general as possible, text isn't directly inserted into choice (There are games that present text before presenting choices separately for example). Instead, you can use an optional #text modifier :

@main
b : "Hello World!"
-> #text:"Choose option 1 or option 2" @choice
|"Option 1"| "Action sequence to execute when this choice is selected.."
|"Option 2"| "Action sequence to execute when this other choice is selected.."
;

You can also control the visibility of choice options with their own set of "core" modifiers and other modifiers that can be passed on to the app using Narra.. :

|#visible:true "Visible Option"| ...
|#visible:false "This option is not presented to the user"| ...

Now that we have choices, how do we jump from tree to tree ? Well, you just use the jump action. Which jumps to another tree using its label. When we reach a leaf of the tree, we just return to the place on in which we performed the jump (unless the narra forest ends)

@tree main
"Text"
-> @jump intro

@tree intro
@choice
|"Option 1"| @jump option1
|"Option 2"| @jump option2
;

@tree option1
"Show 1"
-> @end

@tree option2
"Show 2"

We also snuck in another action above, "@end" ends the dialogue experience.

Sometimes you want to branch out on a value, for that, you can use the "match" statement, which is the same as a switch statement, it selects the option that's equal to the value matched, it's more general than an if statement :

@tree main
"Text"
-> @match true
|true| @jump matched_true
|false| "False text" -> @end

Dynamic values and evals :

In Narra, dynamic computed values are surrounded by << CODE >>. To execute a piece of code on demande, you can use the eval action. The initial version of Narra was written in Rust, so we chose Rhai as a scripting language, but we were easily able to switch from Rhai to Lua. And I personally think that you can "easily" swap it into your prefered language, as long as you're able to eval code chunks and expose a some elements of Narra API.

@tree main
"Text : " <> <<"The rest of this text comes from Lua">>
-> @eval << script:jump("intro") >>

@tree intro
"The jump was performed from Lua"

Narra also keeps track of the seen actions by assigning each action it's own unique id. This is very useful for saving and loading the scripts. Moreover, you can assign your own id with the #id modifier, and then use script:seen("some id") to check if that action was executed before.
Evals are extremely useful with glued strings, as you can expose things like player name and glue it together for dynamic text, or vary text based on external parameters :

@main
Bob : "Well well well, if it isn't " <> <<player_name>> <> ", longtime no see !"

Blocking and non blocking actions :

There are two types of actions, blocking ones by default stop the execution until handle_action is called on the NarraRuntime. Examples of these are the choice and dialogue actions,
non blocking ones like jump, match and eval call the next action after they're done executing.
Sometimes you'll want to block in your evals to do things like showing some tutorial element, async logic ...etc. For that, you can use "@@eval".
You can also use the #block modifier to block or not block, this overrides the default behavior of an action.

#block @eval <<print("hello")>>

We offer some #modifier and #!modifier as syntax sugar for boolean valued modifiers.

More complex logic :

You can always write down additional functions and code in Lua (or whatever language you choose to integrate with Narra).
You can either do that in the same narra file by doing @declare

@declare <<
... my code here
>>

... narra script

or writing it down in a seperate file and including it with @include :

@include <<myfile.lua>>
...

  1. A use case that I've already tested and experimented with. ↩︎

  2. I'm planning to expand glued strings into text fragments that can be individually styled with modifiers. The problems of the approach come with the initial complexity that they introduce for setup. I'm considering possibly handling them as togglable language features. ↩︎

Subscribe to Verbose Sleep

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe