Rune: BetaDwarf's Visual Scripting Language for Unity3DEveryone at BetaDwarf is hard at work producing Forced 2 : The Rush, which is currently in alpha. In the debut title Forced a visual scripting language was created to allow designers to set up world events for in game actions such as creatures spawning, or cutscenes. Based on the usefulness of this tool, it was decided early on in Forced 2 that a visual scripting language was needed. These types of tools are great in that they allow designers and other non programmer members of the team to rapidly create logical content. We primarily use the system to allow designers to create abilities that players and enemies can use to battle each other with, however it's utility can extend far beyond that, as we have attempted to build a very open ended system that allows content creators to add new functionality (with a little programmer help of course) to the system.
Graphs, nodes, pins and variables oh my!When researching the field of visual scripting for games, we looked at one of the best around, Blueprint for Unreal Engine 4. This gave us some great inspiration and helped formulate the basic system we constructed. All of the logic for an ability, or simply an assembly of logic is represented by a Graph. Inside a graph we have a collection of Nodes, each node is comprised of Pins, which can either be of Flow or Variable type. Flow pins connect to other flow pins and drive the logic forward, variable pins are used for variable passing and are essentially the parameters to the method calls represented by the flow pins. We also have variables in the graphs, these can be Graph Variables, which are local to the graph, and each variable pin has a backing variable which stores the intermediate values, which could be used anywhere by the graph.
Below you can see an example of a (section of) graph
The above graph is a section of our Boomer Boom ability. This ability causes an explosion to go off right before the death of the enemy. As you can see we have an event system that we hook into, so OnStart is called when the graph is ready to go, similar to the Start method in
MonoBehaviours. When this event fires, we receive a call and spawn an explosion Subgraph, of type Instantiated Subgraph, which is an object that is instantiated and immediately after a graph is applied to it. We have two other type of subgraphs as well, one is called
Function Subgraph, which is a graph that is run inside the current graph. The other is a
Target Subgraph, which is a graph applied to a specific target, we generally use these for buffs in Forced 2.
I've also included another, slightly more complex graph that shows off our GetterNodes, these are not called by flow, instead they are called at the point they are used by a flow. You can see this graph below, for our Earth Elemental Boss' Level 2 Shard Blast. As our graphs and nodes are often changing you can see one result of this in the graph, the red nodes indicate nodes which have been rendered obsolete in code, and therefore should be changed to use the most up to date versions. However the old versions usually work fine as the graph was initially made with them in mind.
How it's put togetherSomething that we found was possible early one was the ability to hot load code. We write all of our code in C#, which with a little trickery can be compiled dynamically in Unity using
CodeDomProvider. To do this we write all of our code dynamically for the graphs. When a user has reached a point of completeness they are satisfied with they can call for a graph build. When the graph builds we write out code corresponding to each node. This can be done while a game is playing, and you can reload an assembly at runtime. However this is not done for finished abilities, these are written out to a .cs file and included in the project. This has the benefit of being extremely fast, as the generated classes themselves are relatively simple assembly of method calls invoking other C# scripts. I believe this is a benefit to using our system over perhaps a Lua based approach, and we have access to all of C#'s great features.
To give some insight into how we write code see the class definition below. This definition is for one of our node classes, of the type FlowNode. We use attributes to mark up input calls, and output calls. Input calls are always a function, which can return either void or an
IEnumerator (to be handled as a coroutine). Output calls are always of type
Action or
Action<T1...>, in this way we can have the flows connect using callbacks attached to the actions. This is our way of activating the next node in the flow chain. When the graph is set up, we register the input method call in the graph code with the output Action of the method invoked by the previous node's graph method.
using System;
namespace BetaDwarf.Rune.Runtime.Nodes
{
[DisplayName("Flow Utility Nodes/Debug Log")]
[FlowNode]
public class DebugLogNode
{
[OutputFlow(0)]
public Action Complete { get; set; }
[InputFlow(0)]
public void Print([DefaultValue("Default Debug Log Text")] string text)
{
UnityEngine.Debug.Log(text);
if (Complete != null)
{
Complete();
}
}
}
}
With these definitions it is very easy to write out the code, when you know how the nodes are connected and where variable values can be found (as outputs from other nodes or from graph variables). We use reflection at design time to generate the node structure. When a new node is placed in a graph, we reflect the attributes for the selected node type, for example the DebugLogNode seen above. These fill out simple referential data structures, where nodes contain pins, which possibly contain variables. When the code is generated from the in memory node structure, we write calls into the node classes, such as the one seen above.
Below you can see how the flow of the system is constructed in a very abbreviated form. An event is fired by external systems and our listeners cause the event to be sent into a graph, which invokes an input method call. This input method call invokes the InputFlow designated by the node, once this happens it is up to the node class to call an Action which in turn invokes a callback that has been registered with that Action.
An interesting note about variables is that while we can handle most types with normal C# serialization (we tried lots of things here, and this by far worked the best as many other serialization frameworks don't serialize data in the exact type it is, or handle System.Object..Json/Protobuf .NET you disappoint me!) Unity types are impossible to serialize in this way. We circumvent this by maintaining a
ScriptableObject instance in a resource folder to serialize any
UnityEngine.Object reference used by the graph. It is then simply a matter of loading these resources when the graph is started. The graphs are all
MonoBehaviour parented classes, inheriting from a number of interfaces we use to interact with them.
Rune has proven to be an amazing asset for the team, and while it's had it's usability issues, it has allowed the programmers to handle large systems while designers can create content like cards and enemies. We currently build graphs for many things, including our events, giving out cards after arenas are completed even directing game flow. I hope some of you find this interesting and I'm always happy to talk about some of the implementation details if someone is curious!