The last few weeks, my work has primarly been focused around my visual scripting system, so I decided to make a startup article to present the whole system to you.
I assume everyone should know about scripting and its purposes. Whether you are building your own smallscale game(engine), or use a large engine like Unreal/Unity, you will eventually come across scripting, for writing an entire game with cutscenes, etc... in pure C++/any other compiled language is no fun.
For those of you who haven't heard about visual scripting yet, its a step further in terms of scripting. Instead of writing text-files in the scripting language of your choice, you place nodes in a graphical user interface and connect them to form a script. There are many advantages (in my regards), and also some disadvantages over regular scripting, but I'll get to that later.
I used to have AngelScript as my engines scripting language for some time, so props to Andreas Jonsson at that point. While there where not any immediate problems with the language, I felt it didn't fulfill my requirements once I started porting my 2D-action-Rpg to the engine. The main reason was: Cutscenes. For those of you who don't know, the Rpg-Maker that I was using back then had this thing simply labeled "events". It was sort of an early visual scripting, though very limited. You could place an "event" on the map, and then place (predefined) commands in order to form a cutscene. Though poorly in terms of customization, it had some neat features: Like having the ability to place a "wait" command, which would delay execution of the "script" until a certain amount of time, without halting the rest of the application. Customization would happen via "call script" nodes, where you could place RGSS (ruby) script code... which sucked ass on many levels, mainly because the size of the text areas of those nodes was so limited.
So my goal was: I want a similar functionality (wait without hanging the rest of the application, ...), but improve upon it. So I searched for visual scripts, and came across Unreals Blueprint system... and immeditely loved it. So I decided to make my system as a combination of the best of both worlds, and this is what I've ended with now.
The script systems basics:
(DISCLAIMER: My system highly resembles Unreals Blueprints in terms of looks. I took great inspiration from their system, I'm not ashamed to admit it. Graphics have been designed by hand though, and implementation is completely indifferent to theirs)
For regular coders and scripters, there are some details that might be interesting/new. In text, you just write line for line, and without control-structures, the lines are executed one after another. In my visual scripting system, there are two call flows: Execution, and calculation.
Execution of nodes flows from left to right, following the white connections you can see in the image. Each node can only have one input (currently), but can have one or more outputs, which are called as warranted by the node (Eg. Condition calls true or false based on its attribute input). All regular nodes have to be connected to this flow, otherwise they won't be executed.
Nodes can also have attribute input, colored for the specific type. There are the following builtin types:
Then there are application supplied types:
You may notice that those are identical to the types from my typesystem I presented in an earlier article. Yep, this is where that type system originated from.
Regarding the flow on those attributes, calculation/evaluation-order in this case is from right to left. Once the node requests its attribute value, all appended nodes are evaluated to the end, and the values returned until they arrive at their destination. Obviously, there has to be a special type of node that supports this call, which I refer to as "const node" ("pure" in Unreals context).
Following this, you can see how the script executes. Now, there are a few more basic concepts to get a running script though:
Wheres the entry point of such a script, you may ask? Those are the so called triggers. A trigger node is called from application, at a certain point in time, e.g. when the script is first executed, or in each tick. Due to the possibility of a node with a timing (like wait) being in the execution graph, a trigger cannot be assumed to finish its full execution when it returns to the application, therefore return values are not possible. Also, a trigger can only be called once before its connected graph has finished, so therefore calling it again before that will dismiss the call.
Aside from the scripts execution itself, each script is in and of itself a "class", which can have exactly one parent. Before we are to talk about inheritance though, we have to adress all the class-based resources that a visual script has:
A script class can have member variables, just like a regular class. Those have a certain type, can eigther be single or array, can be const, can even be static, can eigther be public, protected or private, and can be setup for automatic serialization (more about all those in a different article). A variable has a default value, and can further be accessed in the call graph, via getters and setters (except for const).
A script class can also have member functions. Those have most brandmarks of traditional c++ members: They can be regular or const (see point about execution/calculation graph), can have attribute input/output, can be static, and can eigther be public, protected or private, as well as (pure) virtual. A method has an in/out-node, and the graph has to be connected between them. Attributes are supplied via in-node, and return-values are taken as connected to the out-node. Trigger nodes cannot be placed in a method, and methods have only access to fitting class properties (ie. no non-const methods in const method, no instance-variables in static method).
Aside from methods, its also possible to give a script class a custom trigger. Those have the same requirements as regular triggers. They can only be placed in the main execution graph, their call (via activation node in this case) will not block, and any additional call while still running will be disregarded. Whats the point of those over methods? Them being non-blocking is the main case. Say I have a virtual "OnUpdate"-method, which checks for the player to jump, as well as some other actions. I can now model the jump as a straight-forward execution of timed nodes - the only problem is, if I do this straight in this or any other method, it will block the "OnUpdate"-call until the jumping has finished. If this is what I want, good, but if I want to execute other code in the OnUpdate-method while jumping takes place, I have to make a "ExecuteJump"-Trigger, and put the jumping code there. Kind of specific and technical, I hope everyone understands this, if not, I'm going to go into more of a detail about the timed nodes in another article.
Now about inheritance. Only single inheritance is allowed. Other than that, its just like C++: You can override and virtual method, and have to override any pure ones. You can call all public/protected parent methods and triggers, as well as access all those variables. One additional requirements is that if an application trigger is placed in the parent, its child cannot also place it. So if a parent wants to execute code on startup, and allow any child to potentially also do that, it has to provide a virtual method (not so sure if this is so good anymore though...).
So, thats about it for the basic of a script-class. In the entity-component system, there is a "Script" component, which allows instantiation of such an event for an entity, which allows the script to execute based on the triggers.
Thats if for this time. Obviously this topic is much more deep just on the design-part of things (not even talking about coding), since estatics and usability is a huge factor for every visual scripting system. So I'll add a few more things in future articles, specially some details about my own implementation (regarding timed events). Thanks for reading, until next time.