How to Implement a Node-based Scripting Language: Part 2

Javascript Runtime Implementation

This is the second part of the series on how to create a node-based scripting language. If you didn’t have the chance to read the first part, you may find it here.

In this post, you’ll learn the very basics needed to implement the runtime side of an interpreted node-based scripting language using es6 javascript. In the next part, you’ll get to read about using C++ to accomplish the same task.

So let’s start! You’ll be surprised at how easy it is.

Events

Events are the driving mechanism of node-based scripting. They control the flow of execution. In reality, they might be nothing more than a function call. And that is how we’re going to start out implementing them.

In reality, they might be nothing more than a function call. And that is how we’re going to start out implementing them.

Let’s say we want to create a very simple language capable of scripting out a simple input/output scenario. that is able to print a message whenever the user presses a key on the keyboard. Such a graph may look like this:

As a start, we want to print a message whenever the user presses a key on the keyboard. Such a graph may look like this:

graph1

When you execute this graph and hit any key, a message is printed out. To model this graph, we define the following node classes:

Now, we need the KeyPress’ pressed event to actually be called whenever the user presses a key. This can be accomplished by adding a simple document key down handler and hooking it up inside the KeyPress constructor:

But what if we want more than one connection, like in the following graph?

graph2

In this case, we can’t simply use a single function variable to store the value. Instead, we’ll create and use event and event handler classes:

We may now update the node implementations to look like the following:

A graph with multiple event handlers can now be constructed like this:

Keep in mind that there is nothing stopping us from hooking the same function onto more than one event.

Variables

Next, let’s have a look at variables. What if we want to configure the message that gets printed? Or emit the key-press event only for a certain key?

We could simply add variables to our nodes in code and make sure to update the implementations as follows:

This now yields a graph that looks like this:

graph3

But let’s consider a more advanced scenario. What if we wanted to use a node to retrieve a value? In this case, we want to retrieve a value from a user input whenever we hit the space key. The graph for this might look something like this:

graph4

Notice the prompt node and the connection to the print message node’s message property.

In order to accomplish this in code, we need to be able to link these values together. So we’ll wrap them inside objects:

And here is the full code for the graph, including our new prompt node:

The print message node will read the value from the very same object instance where the prompt node wrote it. This is thanks to the use of the input and output objects wrapping the values.

Multiple Inputs

The above works if we want to connect an output to more than one input but an input is better limited to one output connection to avoid ambiguities.

The exception here is if we wanted to use multiple inputs and have them become an array. Here, the print message node might be allowed to accept multiple values and concatenate them. So we have something like this:

graph5

The code for print message needs to be updated to support multiple inputs explicitly:

Notice that the message input is now an array. The input array class looks like this:

If you’re not familiar with generators, you might wonder what the * is. Well, together with the yield keyword, it allows the definition of an iteration mechanism. In this case, we’re able to unpack the output values, so that we don’t have to do that in the code that’s using this class.

To be able to put in string constants (gray nodes), we simply use the output class to represent them. The graph model is now created like this:

So when the user presses spacebar, an input dialog pops up asking for his name. Then the print message node prints out the message ’Hello {entered name}!’. This string has been computed by concatenating the prefix, the entered name, and the postfix.

Ordering

If you were paying close attention in the last section, you might wonder how the viewer of the graph alone will be able to determine the order in which the strings get added together. There’s nothing in the graph showing any sort of ordering provided that the user can normally place nodes in any order. We might use the convention of letting the items get added in the order that the user added them, but this doesn’t help somebody else that is looking at the graph.

There’s nothing in the graph showing any sort of order, provided that the user can normally place nodes as he wishes.

Solving this problem is user interface territory that we’ll cover more closely later. There are a few options on how this can be solved, but for now, we’ll just add some numbers to the connections.

The same can be done for connections on an event. Although in this case, we might also say that the order is undefined. If the order is important it needs to be made explicit by the user, possibly by enforcing the creation of a sequence as in Unreal blueprints.

Persistence

Now that we have been touching the core runtime model, I briefly want to touch on how we might store the node graphs in a JSON format. Since the nodes are internally referencing each other, we can’t just store them directly. Instead, we’ll be splitting up nodes and connections.

This is how the last graph would look in JSON:

This is also the foundation of the editing model. It describes the graph slightly different compared to the runtime model.

The code for loading might be something like the following:

The finished design

The finished design looks like this:

design

Using these simple building blocks, we can construct many types of nodes.

What’s next?

Next up we’ll be looking at a similar implementation using C++. The reason we are moving on to C++ here is that I want to discuss some performance implications of an interpreted node based scripting language.

We’ll also look at how we could implement subgraph support as well as applying types for inputs and outputs.

After that, we’ll move on to the editing side of things and create a simple browser-based editor.

Hang in there!

And if you didn’t already do so, you might want to sign up for the mailing list so you don’t miss any new posts.

About The Author

Theresia Hansson

Theresia Hansson has worked several years as a software engineer within the game industry and on several game engines, including EA's Frostbite Engine. She is passionate about tools and workflows for creating games and loves to share her knowledge with the community.

Add a comment

*Please complete all fields correctly

Related Blogs

node-based scripting
Posted by Theresia Hansson | October 8, 2017
How to Implement a Node-based Scripting Language: Part 1
Requirements and Design   So you want to learn how to create a node based scripting language? You know, one of those languages that have become extremely popular in today’s...