Search website

Article : Node Based Scripting

Node-based scripting is not a traditional scripting language. And it’s not a new idea, though it seems like people have forgotten about it. New systems appear – see Unreal Engine’s Kismet – which people think are amazing new technologies. They aren’t new; just amazing. I first encountered this scripting system in the quake engine. It was very crude, and limited, but it had all the basic properties of the system I am about to describe.

A brief note : Throughout this article, I use my own engine, Jemgine, and it’s level editor for my examples. Jemgine is available at http://jemgine.codeplex.com You’ll need to have XNA Gamestudio 3.1 installed to compile it, and to run the editor.

The target user of a node-based scripting system isn’t the programmer, it’s the level designer. The level designer might not know how to program. They might be constantly bothering a programmer to write scripts for them. Node-based scripting is an attempt to give the non-programmer level designer tools they understand and can use to create interesting behavior in a level without involving the programmer, and without having to learn a complex scripting language.

Node-based scripting is a visual programming language. Most visual programming languages make the same mistake. They try and present the nuts and bolts of programming in a visual manner. Programs are still made of for loops and variables, only they’re little boxes on the screen instead of text. They require the same skill set as programming in a text editor, except it takes longer to put the program together. Understandably, they aren’t terribly usefull languages. The target audience doesn’t know how to program – getting rid of the text editor doesn’t change that. And those that do know how to program aren’t going to give up the text editor for a less productive environment. Node-based scripting doesn’t have to fall into this trap.

First, what is a node? A node is a little atom of behavior. It’s an action that can be applied to the objects in the game world, or a chunk of logic. It’s important to strike a balance somewhere between the low level and the high level. If your nodes represent loops and variables and conditions, you’ll fall into that trap I described earlier. Make the nodes too high level, and you’ll lose their flexibility. It can be beneficial to have some of those low level constructs, and some of the high level. Focus on the middle ground, but understand that you’re going to end up with a fair mix of both.

Nodes have inputs and outputs. These are represented by terminals. Output terminals are connected to input terminals. Nodes also have references. These reference terminals are connected to other nodes or game objects. For example, the node ‘MoveGameObject’ has one input to trigger the action. It has two outputs – Began and Arrived, which are triggered when the movement begins and when it ends, respectively. It also has two references. One that’s connected to the game object that needs moving, and another that’s connected to the waypoint to move it to.

A node can be as simple in code as a list of terminals and a virtual function. Like this.

You’ll need a way to create the script. You can do it in code, or you can do it in XML, but since the whole point of this system is to make it easy for your level designers to use, you should do it in your level editor. The level editor only needs to support two basic operations. Creating nodes, and connecting terminals.

You can see this in action within the Jemgine level editor. In the editor, press Alt+S to create a new script node. Then, with that node selected, hold alt and left click and drag an output terminal to connect it to an input terminal. This is probably the most important part of the system, so why aren’t I spending more time on it? Because it’s a terribly complex system and it’s way beyond the scope of this article. But, I do suply a reference implementation in the form of the Jemgine level Editor.

The other half of the system is the engine that executes a script in the game. At first blush it seems that this would be very complicated, but it turns out it’s the simplest piece of the puzzle. Because it’s so simple, here is the entire implementation.

You might not need a script engine at all. When a script is fired, you could just call FireInput on it yourself. The benefit of a scripting engine is that it can delay the execution of a fired input until the next update cycle, which will prevent a bad script from creating an infinite script loop and freezing the game. This is why the Update function makes a copy of the fire list.

The only piece of the puzzle left is getting these scripts from the editor to the game. I use XNA’s built in XML serializer. Actually, this is the only part of the article that actually depends on XNA. The XML Serializer is great, and it’s already integrated into the content pipeline. But it has a few flaws. Notably, our script is a graph, and the XML Serializer can’t handle a graph. We have to flatten it first, by which I mean replace all the references with simple indicies we can use to restore the references at load time. You could keep the script in a flat format all the time, but I found it was easier to flatten and restore than to work with the flat format all the time.

Flattening isn’t complicated. First, assign all the objects an id. Then, copy the id of the referenced object into the terminal. The XML Serializer can be setup to serialize that id, and not the reference.

If game objects and entity components are themselves script nodes, this will allow you to attach scripts to all sorts of things. A health component, for example, could have an output terminal that it fires when the game object it is attached to dies. Now a script can be attached to an object’s death very easily.

In a traditional scripting system, it can be pretty hard to tell what’s going on in a level, especially if you aren’t the level designer that built it. A node-based scripting system solves this problem. In a traditional system, what you see in the level editor is a filename. With this node-based system, the script is right there, in the level. This can be a disadvantage too.

It’s a pain to build a complex script over and over for different levels, so the editor should be able to import a script into the level. The Jemgine level editor can import entire levels into another, so I can create a level that includes not just the script but the objects it operates on. This solves one of the disadvantages, but if there is an error in the original script, fixing that script won’t fix every level that uses it. We’ve traded this ‘fix in one place’ for a bit of additional flexibility. Because the script is copied into the level, we can make customizations to the script specific to that level.

When you first start working with a scripting system like this, it’s going to seem like there is no advantage at all. You’ll still have level designers taking up valuable programmer time, but instead of scripts the programmers will be writing nodes. Eventually you’ll build up a large library of script nodes and the level designers will find fewer and fewer things to bug the programmers about.

At first, the level designers just won’t get it. You probably won’t get it either. But after you write a few scripts, after you solve a few problems, it will click, and soon your level designers will be doing things you had never imagined.

This is a script for a door I created in the Jemgine level Editor.

In this script, there are two game objects. One is the door itself, the other implements the trigger zone for the door’s button. The nodes labelled ‘PolygonPhysics’ are entity components attached to the game objects that interact with the engine’s physics sub system. When the player touches the trigger zone, the PolygonPhysics component fires it’s OnContact output. This is connected to a UseAction node, which will wait until the player presses a button before firing it’s output. So if the player is touching the trigger zone, and presses the button, the UseAction node will fire that relay, which fires four other nodes. MoveEntity does exactly what you would expect. Disable disables the original switch, so the player can’t press it again. Show and Hide work together with two other game objects (Switch On, and Switch Off) to change the switch graphic. Finally, MoveEntity plays some sounds.

Leave a Reply

Switch to our mobile site