I spent around ten weeks on implementing visual scripting. I aimed to replicate the functionality and workflow of Blueprints, to make gameplay programming accessible to non-technical designers.

What can this tool do?

I ensured that you can write a game entirely using C++, entirely using scripts or a mix of both. This allows programmers to develop complicated or computationally expensive gameplay elements, while designers can prototype and rapidly iterate using the scripting tool.

The visual scripting enabled designers to implement player movement and the many abilities featured in Lichgate, all without needing assistance of programmers.

In the end, more than 90% of the gameplay in Lichgate was achieved through visual scripting, powered by 156 different scripts.

The tool supports for-loops, while-loops, branches (if/else), getting & setting variables, function calls, and events.

Script Components

Scripts can be added as components to entities. To achieve this, I made some modifications to EnTT, the ECS library that we used. Now, script components are stored in the same way as C++ components. You don’t need to handle both C++ and script classes for the inspector and serializer, because you access them using the same interface!

C++ Bindings

I made the API for exposing C++ to scripting as simple as could be, using the runtime reflection system I’d written earlier. And of course, scripts can always interact and communicate with other scripts.

type.AddField(&Player::Health, "Health")
  .GetProperties()
    .Add(Props::sIsScriptableTag);

type.AddFunc(&Player::Respawn, "Respawn")
  .GetProperties()
    .Add(Props::sIsScriptableTag)

You can also go the other way; call and access script functions and fields from C++. This was especially useful for writing and maintaining the dozens of unit-tests that tested the visual script interpreter.

Editor

I designed the workflow and the editor to be similar to Unreal Blueprints. I used the Node Editor in ImGui library as a starting point.

Context aware search

I created a responsive and context aware search bar, greatly improving ease of use and development speed. The searching happens fully asynchronously. Options that are picked more frequently, or options that make more sense given the existing function, are ranked higher.

Shortcuts

I included useful shortcuts; nodes can be copied and pasted, allowing for rapid refactoring and expansion of your scripts. Every action can be undone/redone. Reroute nodes can be used to clean up your scripts

Errors

Any runtime or compile time errors in your scripts will be printed to the console. Pressing it navigates the focus directly to the source of the error.

Virtual Machine

The Visual Scripting uses a node based interpreter. The virtual machine can be found in its entirety in VirtualMachine.cpp.

Type erasure

Everything in the virtual machine is, from a C++ perspective, completely type-erased. C++ objects are represented using a MetaAny, essentially a void* with some additional type information. To give the virtual machine the ability to still call C++ functions using these, I used C++ templates. For those interested, a significantly simplified standalone implementation can be found below.

The actual implementation of MetaFunc is more complete, it includes type-safety and various optimisations. I extended C++ copy-elision to construct the return value of nodes directly into the right place in the stack, without having to copy or move the variable at all.

The full implementation can be found here:

Graph traversal

For traversing the graph, I follow the execution links (the white lines), which indicate the order to execute nodes in. The output of these executed nodes are stored directly on a stack. Variables stored in this stack can be passed on to future nodes that need to be executed.

Pure nodes, nodes without an execution link, are executed whenever its output is required for another node. Because pure nodes do not modify the state of the world or engine, their results are cached; no need to call that function again until we run an impure node and the state of the world or engine changes.