Unigine::Schemer
Schemer is a visual scripting system that allows a developer to create complex sequences of gameplay events without having to script them manually. This way the artists can add interactivity to the world without the help of a programmer. By connecting paths in a flow graph, it is possible to script cutscenes, switches, timers, changes in lighting and much more.
Schemer (as well as Skinner) can either be loaded inside Unigine window or in the external widgets.
Schemer scripts can be found under data/core/systems/schemer folder.
Overview
Schemer flow graph consists of the following entities:
- Blocks are the main entities of the Schemer. They describe events, operations or variables of different types. The gameplay programmers code these actions depending on the project needs and make them available to be used in the Schemer by artists. For example, a block can describe the behavior of the game object, for example, the explosion sequence of a game vehicle.
There is also a number of predefined blocks that perform various arithmetic operations, print a message, etc. - Nodes are Block units that form the graph sequence; they can be dragged around the graph canvas. That is, while Block is the type of the node that determines what action to do, a node is a block that has been added to the graph to perform this action, given the exact input data. Nodes of the same Block differ in their variables and states.
- Nodes are connected together by Joints. Joints can be of two types:
- Paths are rendered in white. They control the sequence of executing nodes when the Schemer script is run. That is, by moving along the path joints we could trace how the graph is unrolling starting from each possible entry point (the first nodes of the script) and what are the next triggered nodes.
- Links are rendered in green. They simply connect nodes that store values and do not perform anything by themselves.
- The point of connection is called an Anchor. Anchors can be of different shapes (round for output paths from a node and square for input paths into a node).
- Schemer editor is a canvas on which the created graph is drawn. It uses Unigine::Widgets::Graph classes for drawing.
How To Use Schemer Graph
To create a graph, drag the blocks onto editor canvas.
- The first node to start the graph with should always be schemer.entry. The name of this entry point is specified in the world script. By default (in Schemer sample), main name should be specified.
- Put the cursor over the Output anchor and drag the joint to the Input anchor of the next node. Paths that connect Output and Input anchors control a sequence of executing a graph.
- Values that nodes operate on are marked in green. The value on the right side of a node is an output value that contains the result of the operation performed by this node. The value on the right side is an input value. Connect two values to pass a value from one node to another.
After the Schemer graph has been created or modified, click Run button. A script will be generated out of the graph, compiled on-the-fly and run.
You can also save the created script into *.script file to be loaded and edited later or executed at runtime.
Blocks
Inside blocks are functions or snippets of UnigineScript code. The resulting script created from a Schemer graph is made up of code snippets from all blocks put together one after another. When blocks are processed, separate namespaces are created for each of the block to avoid name collisions.
Just like any other script, a block has a number of functions to control its behavior:
- init is for code executed on the Schemer script initialization. It can be used to initialize heavy resources: for example, Skinner loads animations and creates buffers in the init code.
- shutdown is for code executed on the Schemer script shutdown.
Both init() and shutdown() code snippets do not depend on the graph execution sequence. All blocks will be initialized or shout down at once (see the details). - update is for code executed when the graph is executed and it is the turn for node with the current block to be activated.
- common is for functions and variables that should have a global scope inside of the block.
Any of these functions inside of the block can be omitted, if necessary.
Blocks File Syntax
Blocks are described in a *.blocks file of XML format. For example, the default blocks are contained in the data/core/systems/schemer/blocks/schemer.blocks.
Besides functions to execute, a block also describes its joints:
- Input and output paths (input_path and output_path) to connect a node with this block to other nodes.
- Input and output links (input_link and output_link) are used to receive variables from, and pass them to other nodes.
<?xml version="1.0" encoding="utf-8"?>
<blocks version="1.00">
<block type="schemer.message">
<input_path>input</input_path>
<output_path>output</output_path>
<input_link>value</input_link>
<init>log.message("schemer.message: block is initialized with a script\n");</init>
<shutdown>log.message("schemer.message: block is shut down with a script\n");</init>
<update>log.message("schemer.message: %s\n",typeinfo(value));</update>
</block>
</blocks>
If the code should contain characters like ">" or "&" that are illegal in XML elements, use CDATA to indicate a section ignored by the parser.
<update><![CDATA[
if(a > b) goto output_gt;
if(a < b) goto output_lt;
goto output_eq;
]]></update>
The available attributes for joints (both paths and links) are as follows:
- label — text at the anchor point
- align — position of the anchor point on the specified side of the node. Possible values are left, right, bottom.
- value — the default value to use.
- mask — bit mask for connecting only the anchors with a matching mask. For a joint to be created between two anchors, at least one bit of masks should match.
Customize Basic Blocks
If you want to customize any of the available blocks, you need to edit data/core/systems/schemer/blocks/schemer.blocks. There, you can modify init, update or shutdown functions written in UnigineSript, add anchors to connect to other nodes, etc.
Adding a Custom Blocks File
If you want to expand the list of predefined blocks, you can add custom blocks files. New blocks can be loaded via loadBlocks(string name) function in the Schemer constructor (data/core/systems/schemer/schemer.h file).
Available Schemer Blocks
The following blocks are the basic Schemer blocks defined in the data/core/systems/schemer/schemer_blocks.h script.
schemer.entry | This is a start-up point for running a graph. There can be multiple entry points in one graph. What point to use to start the graph is defined from the world script. It has the following parameter:
|
---|---|
schemer.input | Passes the variable to the next Schemer node.
|
schemer.output | Stores the variable received from the previous Schemer node.
|
schemer.boolean | Set the true (checked) or false (unchecked) value. |
schemer.constant | Sets a variable.
|
schemer.file | Stores the file name.
|
The following blocks are the predefined Schemer blocks loaded from data/core/systems/schemer/blocks/schemer.blocks file.
schemer.abs | Outputs the absolute value of the input value. For example, if input value is -2, the output value will be 2.
|
---|---|
schemer.acos | Outputs the arc cosine of the value. It is the inverse cosine function, which means that a == cos(acos(a)) for every value of a that is within acos()'s range.
|
schemer.add | Adds values. The resulting value = a + b.
|
schemer.and | Performs a boolean logical AND on input values. Evaluates if both operands are true; otherwise, false is output.
|
schemer.asin | Outputs the arc sine of the value. asin() is the inverse sine function, which means that a == sin(asin(a)) for every value of a that is within asin()'s range.
|
schemer.atan | Outputs the arc tangent of the value. atan() is the inverse tangent function, which means that a == tan(atan(a)) for every value of a that is within atan()'s range.
|
schemer.atan2 | Outputs the arc tangent of two input values. It is similar to calculating the arc tangent of y / x, except that the signs of both arguments are used to determine the quadrant of the result.
|
schemer.band | Performs a bitwise AND on two integers. It compares each bit of its first operand to the corresponding bit of the second operand. Each bit in the result is 1 only if both corresponding bits in the two input operands are 1.
|
schemer.bor | Performs a bitwise inclusive OR on two integers. It compares each bit of its first operand to the corresponding bit of the second operand. If both of the corresponding bits in the two input operands are 0, the result of that bit in the result is 0; otherwise, if either of the bits is 1 the result is 1.
|
schemer.bxor | Performs a bitwise exclusive OR on two integers. It compares each bit of its first operand to the corresponding bit of the second operand. If both bits are 1 or both bits are 0, the corresponding bit of the result is set to 0. Otherwise, the corresponding result bit is set to 1.
|
schemer.ceil | Calculates the smallest integral value that is not less than input value. For vectors values are obtained per component. For example, if input value is 2.4, the output value will be 3.
|
schemer.clamp | Clamps a value within the specified min and max limits.
|
schemer.clock | Returns the time in seconds passed from the application start-up.
|
schemer.copy | Copies an input variable to be passed to further nodes. A node can copy its variable only to the next node that it triggers. This node can serve as a temporary buffer to store the variable between node run-times.
|
schemer.cos | Outputs the cosine of the value.
|
schemer.counter | Increments value of the counter. The default counter value is 0 (changed to 1 when the counter node is triggered).
|
schemer.cross | Outputs cross product of two input vectors.
|
schemer.delay | Triggers an event (a next node) after the specified interval has passed. (See also schemer.timer for a constant trigger.)
|
schemer.div | Divides the values. The resulting value = a / b.
|
schemer.dot | Outputs dot product of two input vectors.
|
schemer.dot3 | Outputs dot product only of three components of vectors. In case an argument is a four-component vector, its w component is ignored.
|
schemer.event | Generates an event if the input value has changed. Otherwise, output exit is used.
|
schemer.exp | Calculates the exponent of the value.
|
schemer.floor | Rounds an input value down to the nearest integer. For vectors values are obtained per component. For example, if input value is 2.4, the output value will be 2.
|
schemer.frac | Calculates the a fraction part of fraction part of the input value. For vectors values are obtained per component. For example, if input value is 2.4, the output value will be 0.4.
|
schemer.if | Compares two input values and uses different outputs depending on the results of comparison.
|
schemer.length | Calculates the length of a vector.
|
schemer.length2 | Calculates the square length of a vector.
|
schemer.lerp | Calculates the linear interpolation between two values according to the following formula: a + (b - a) * k
|
schemer.log | Calculates a natural logarithm of the value.
|
schemer.log10 | Calculates base-10 logarithm of the value.
|
schemer.loop | Repeats the triggering of the another node for a certain number of times.
|
schemer.mad | Multiplies the first two input values and adds the third one to the result. The resulting value = a * b + c.
|
schemer.mat4 | Constructor for a matrix (mat4).
|
schemer.max | Finds the greatest value.
|
schemer.message | Prints a message into the console in the format schemer.message: <vale_type>: "<value>".
|
schemer.min | Finds the smallest value.
|
schemer.mod | Performs a modulo operation (calculates the remainder after division of one value by another).
|
schemer.mul | Multiplies two input values. The resulting value = a * b.
|
schemer.normalize | Normalizes a vector.
|
schemer.or | Performs boolean logical OR on input values. This operation results true if both operands are true and false if both are false.
|
schemer.pow | Calculates the exponential expression.
|
schemer.quat | Constructor for a quaternion (quat).
|
schemer.rand | Generates a random integer value.
|
schemer.rcp | Inverses a value. For vectors values are obtained per component. The result = 1.0 / value.
|
schemer.rsqrt | Inverted square root. The result is one divided by the square root of value.
|
schemer.select | Allows to switch between two output values depending on what trigger value has entered the node. If the trigger value does not match any of the cases (i.e. 0 or 1), the default value is output.
|
schemer.select3 | Allows to switch between three output values depending on what trigger value has entered the node. If the trigger value does not match any of the cases (i.e. 0, 1 or 2), the default value is output.
|
schemer.select4 | Allows to switch between four output values depending on what trigger value has entered the node. If the trigger value does not match any of the cases (i.e. 0, 1, 2 or 3), the default value is output.
|
schemer.select5 | Allows to switch between five output values depending on what trigger value has entered the node. If the trigger value does not match any of the cases (i.e. 0, 1, 2, 3 or 4), the default value is output.
|
schemer.sign | Outputs the sign of the input value. If a positive value or 0 is provided, the output result will be equal to 1. If a negative value is input, the result will be -1. For vectors values are obtained per component.
|
schemer.sin | Outputs the sine of the value.
|
schemer.sqrt | Outputs the square root of the value.
|
schemer.state | State machine with two states. It allows to create different reactions to the same input trigger. The state machine automatically choses the output path depending on its stored state. By default the state is equal to 0. Meaning, if the node is entered using input anchor, it will exit using out 0. When a node is entered via in 1, the machine will change its state to 1 and exit using out 1. Next time the node is triggered via input, it will automatically exit via out 1 again, as the state stores its last changed value.
|
schemer.state3 | State machine with three states. It allows to create different reactions to the same input trigger. The state machine automatically choses the output path depending on its stored state. By default the state is equal to 0. Meaning, if the node is entered using input anchor, it will exit using out 0. When a node is entered via in 2, the machine will change its state to 2 and exit using out 2. Next time the node is triggered via input, it will automatically exit via out 2 again, as the state stores its last changed value.
|
schemer.state4 | State machine with four states. It allows to create different reactions to the same input trigger. The state machine automatically choses the output path depending on its stored state. By default the state is equal to 0. Meaning, if the node is entered using input anchor, it will exit using out 0. When a node is entered via in 2, the machine will change its state to 2 and exit using out 2. Next time the node is triggered via input, it will automatically exit via out 2 again, as the state stores its last changed value.
|
schemer.state5 | State machine with five states. It allows to create different reactions to the same input trigger. The state machine automatically choses the output path depending on its stored state. By default the state is equal to 0. Meaning, if the node is entered using input anchor, it will exit using out 0. When a node is entered via in 2, the machine will change its state to 2 and exit using out 2. Next time the node is triggered via input, it will automatically exit via out 2 again, as the state stores its last changed value.
|
schemer.sub | Subtracts one input value from another. The resulting value = a - b.
|
schemer.switch | Allows to switch between two output variants depending on the input value.
|
schemer.switch3 | Allows to switch between three output variants depending on the input value.
|
schemer.switch4 | Allows to switch between four output variants depending on the input value.
|
schemer.switch5 | Allows to switch between five output variants depending on the input value.
|
schemer.tan | Outputs the tangent of the value.
|
schemer.time | Returns the current system time since the Epoch (00:00:00 UTC, January 1, 1970), measured in seconds.
|
schemer.timer | Triggers other nodes in a specified intervals until stopped. (See also schemer.delay for one-time trigger.)
|
schemer.value | Allows to set a stored constant value to a number of other nodes.
|
schemer.vec3 | Constructor for a three component vector (vec3).
|
schemer.vec4 | Constructor for a four component vector (vec4).
|
schemer.xyz | Allows to access and output X, Y and Z elements of a vector separately. This node can take vec3, vec4, quat and mat4 (if necessary).
|
schemer.xyzw | Allows to access and output X, Y, Z and W elements of a vector separately. This node can take vec4, quat and mat4 (if necessary).
|
How to Run Schemer Script
To run the Schemer from the script (here, without a visual editor), you need to add the code as in the following example. update() function of the script compiled from a graph. It will execute all its nodes one by one in the specified order.
#include <core/scripts/utils.h>
#include <core/systems/schemer/schemer.h>
/*
*/
int init() {
// Use Schemer namespace.
using Unigine::Schemer;
// Create a Schemer.
Schemer schemer = new Schemer();
// Create Schemer script that will compile an executable script out of the graph.
SchemerScript script = new SchemerScript(schemer);
// Load the previously created a graph.
script.load("samples/schemer/scripts/delay.script");
// Compile the loaded graph into a script.
script.compile();
// Run <init></init> code of all blocks used in the script. Here,
// constants in schemer.constant blocks are initialized. This line could be omitted,
// if not necessary.
script.init();
// Run the graph.
script.update(script.getEntryID("main"));
return 1;
}