(this document is up to date for version 3 of the plugin)
Variables, Expressions and Drivers
If you are interested in using
the power of math to control your scene, this is the right place to be. Variables, Expressions and Drivers provide many ways to script your scene. While not as powerful as a fully visual or language-based scripting system, you can still script a lot of features just with expressions. All without writing custom code. Is some ways they are similar to hooking up tons of LogicBricks together, but at least for me personally in a much more readable and easier to organize approach. You could use them to:
- Display values of any public atom properties (anything available to triggers) on UI elements, using custom formatting.
- Smoothly transition a value when flipping a switch, then use that to drive... anything... in the scene.
- Implement a light blinking according to parameters coming from the scene's state.
- Drive morph values by other morphs, user-facing sliders, or really... anything.
Global Variables
To use the system, first you need to define some
Variables. Let's start with
Global Variables, available on the
Variables main tab. Current version of the plugin supports only scalar (single-value) variables. Use one of the buttons at the top to add a new variable, which can be of one of these types:
- Boolean: True/False value. Usually represented as on On/Off toggle in VaM.
- Integer: 32-bit integer (whole number) value. Usually represented as a slider limited to whole numbers in VaM.
- Float: A real number value (stored as a 32-bit floating-point). Usually represented as a slider in VaM.
- String: Any text
The data type of a variable is fixed once a variable is added.
Each new variable will add a section to the variables UI, where you can move the variables up/down (purely for UI organization purposes), collapse the variable block (also just a UI thing). You can also rename a variable or delete it, as long as it's not used from any expression elsewhere in the plugin.
The dropdown box on the variable section separator lets you choose the variable's source. That is - where the variable gets its value. This is where the fun begins.
Variable Sources
There are several sources available to variables of any types, and few sources specific to each type. The common sources are:
- Constant:
A simple constant value. For booleans, you will get a toggle to set the value, for integers/floats a slider, for strings an edit box to type the text into. Rather simple so far.
- Public Value:
At first glance, this might look the same as a constant. The UI to set the value will be the same. The difference is that the plugin will generate a triggerable JSON param for each public value. This means you can set the variable's variable via triggers from anywhere outside of the plugin. The triggers will be called "Set Variable xxx", where xxx is the variable's name.
- Param:
In a way similar to public value, but inverted. Instead of creating a public JSON param that can be set via triggers from outside, this one will let you track any other public JSON param and have the variable automatically follow its value. The param can come from any atom, not just the atoms controlled by the plugin. If you can trigger it, you can track it in a variable.
- Expression:
This is where the fun begins. Expressions will let you type in a mathematical expression, and the variable's value will be its result. Documented in more detail below. Available for boolean, integer, or floating-point. Not for strings.
- Formatted String:
Alternative to expressions available for strings. It lets you include any variable/expression values inside a nicely formatted text. Also explained in more detail below.
- Built-inValues:
Internal values set by the plugin. Different options are available based on the type. Booleans have none in the current version.
Built-in Sources
Note: some of the sources are links to Unity's documentation, where the variable source matches Unity's feature.
Integer Variables
- Frame Index: A value which increments by one for each frame. Similar to Unity's Time.frameCount, but tracked internally by the plugin. It starts at 0 when the plugin is loaded, and increments only when the plugin is enabled. It increments even when the simulation is paused.
- Pose Index: Index of the applied SceneDirector pose.
- Pose Count: Total number of poses defined in the SceneDirector.
- Pose Index in Chapter: Pose index within the set of poses which belong to the currently applied SceneDirector chapter.
- Chapter Length: Total number of poses which belong to the currently applied SceneDirector chapter.
- Chapter Index: Index of the currently applied SceneDirector chapter.
- Chapter Count: Total number of chapters defined in the SceneDirector.
For an example of the pose/chapter-related variables, see the
DildoLanguage Demo Scene. They are used to construct the texts on the buttons on the left side of the control UI.
Float Variables
- Scene Time: Current "game" time in seconds. Similar to Unity's Time.time, but tracked internally by the plugin. It starts at 0 when the plugin is loaded, and increments only when the plugin is enabled. It increments even when the simulation is paused.
- Simulation Time: Similar to the above, but does not advance when the simulation is paused by the Freeze Motion/Sound button or other means.
- Real Time: Real time in seconds. Starts at 0 when the plugin is loaded, then advances at the speed of the computer's clock, ignoring any Unity's quirks such as pausing or simulation speed multipliers.
- Last Frame Duration: Same as Unity's Time.deltaTime. Useful when smoothly updating variable values over time in expressions.
The screenshot at the top which is used to show the Global Variables UI displays some frame time-related fun, using the Last Frame Duration to compute and then smooth current fps. The exponential-smoothing expressions is just some dark magic which ensures the smoothing happens at the same speed regardless of the current framerate (but it's not a simple linear smoothing).
String Variables
- Pose Name: Name of the applied SceneDirector pose.
- Chapter Name: Name of the currently applied SceneDirector chapter.
Expressions
A custom mathematical expression can be used to compute a variable's value. The same expression system is used for booleans, integers and floats. Internally all expressions are evaluated as floating points and converted as needed:
- Float/Int -> Boolean: Zero = False (Off). Any non-zero value = True (On)
- Boolean -> Float/Int: False is converted to zero (0). True is converted to one (1).
- Float -> Integer: The value is rounded to nearest integer
You can see some examples of the expressions in the screenshot at the top. They support all you'd expect and them some extra bells and whistles.
Operators
The usual unary and binary operators are supported, as well as few less than usual, with the expected operator precedence rules.
Arithmetic operators:
- Unary minus operator: - a
- The usual binary operators for addition, subtraction, multiplication, division: a + b, a - b, a * b, a / b
- Power operator: a ^ b
- Modulo operator: a % b
Logical operators. As explained above, in floating-point context, the results of these is 0 for False and 1 for True:
Value comparison:
- Value comparison operators: a == b, a != b, a > b, a < b, a >= b, a <= b
Functions
The following functions are implemented by the expression system. Most of them match standard C# or Unity functions, so the list below links to the corresponding docs rather than explaining everything here again.
- sin(a): Unlike Unity, the parameter is in degrees. Same applies to all other trigonometric functions below
- cos(a)
- tan(a)
- asin(a): Unlike Unity, the returned angle is in degrees. Same for acos and atan.
- acos(a)
- atan(a)
- atan2(y, x)
- deg2rad(a): Converts degrees to radians.
- rad2deg(a): Converts radians to degrees.
- sqrt(a)
- abs(a)
- exp(a)
- log(a): Warning. In version 3 log and log10 are swapped in the implementation, will be fixed in v4.
- log10(a)
- round(a)
- round(a, b): Round to specified number of digits. B will be automatically restricted to an integer between 0 and 15.
- ceil(a)
- floor(a)
- int(a): C#-style explicit conversion from float to int, round towards zero
- sign(a)
- saturate(a), clamp01(a)
- min(a, b)
- max(a, b)
- clamp(value, min, max)
- lerp(a, b, t)
- lerpUnclamped(a, b, t)
- lerpAngle(a, b, t)
- advance(value, target, delta)
- advanceAngle(value, target, delta)
Conditional function:
- if(condition, trueExpression, falseExpression)
This is similar to operator ?: in languages such as C++ or C#. First the condition expression is evaluated. If it's True, then the trueExpression is evaluated and that's the result. Otherwise the falseExpression is evaluated and that's the result.
Self-referencing Variables, Recursion
It is allowed the reference the variable in its own expression. It will not result in an infinite recursion. Generally the logic is that
each expression is evaluated at most once in each frame. This means that self-referencing expressions can be used to smoothly update variable values per-frame. For example, to smoothly advance an
angle at given
speed, you would do this:
- Define a speed variable. Either a constant, or a public value, or a param value. The interpreation is "angle change per second".
- Define a frameTime variable, with the builting source "Last Frame Duration".
- Define an angle variable, with this expression: (angle + frameTime * speed) % 360
The module is there to keep the value reasonably large, since angles wrap around at 360 degrees anyway. It really depends on what you will use it for.
Another example. You have a user-defined value, maybe from a slider. But you want to move towards that value smoothly when the player changes it too fast:
- Define a targetValue variable. Probably as a public value, controlled by a slider via triggers for example.
- Define a frameTime variable, with the builting source "Last Frame Duration".
- Define an smoothValue variable, with this expression: advance(smoothValue, targetValue, 10 * frameTime)
This means that the smooth value will always move towards the target value, at change by at most 10 per second.
A bit more technical detail about the internal update system:
Internally the expression system is optimized as much as possible without generating code at run-time (which is not allowed in VaM). The general flow is, once per each frame:
- The "source" variables are refreshed if needed. These include constant variables, public variables and param variables.
- Only if a source variable's value changes, active drivers which use the variable (directly or indirectly) are marked for evaluation.
- Marked drivers are evaluated, and recursively any expression variables needed by those drivers.
As a consequence, any expressions is evaluated
at most once per frame. But if an expression is not needed by any of the active drivers, it won't be evaluated at all.
Formatted Strings
Instead of expressions, strings support a "formatted string" source. This is basically a text which can reference other variables, which will then be replaced into the text. The syntax of variable replacement is similar to
C#'s interpolated strings, but it's a bit more limited:
- Only names of other variables can be used in the {} interpolated item, no expressions. But the variable itself can be an expression of course.
- There is no width field, only the format string.
- To include the { character in the formatted string literally, double it, just like you would in C#. But do not double the closing }.
Or in other words, the string can contain items using the syntax of
{variable} to include that variable's value in the text, or
{variable:format} to include the variable's value using the specific format.
The formatting string is interpreted depending on the variable's type, similar to the C#'s ToString function:
- int/float: The usual C#'s numeric formatting is supported. Probably the most useful is simple {variable:F3} to print a floating-point value with 3 decimal digits (or any other number than 3).
- string: No formats are supported, always returns the text as is.
- boolan: With no format provided, returns "True" or "False".
Special formatting is supported for booleans to allow for custom true/false strings: if use syntax like {variable:On;Off}, it will convert to "On" for true values and to "Off" for false values.
Technical Note: at a technical level, booleans are converted to integer, 1 for true values, -1 for false values, and converted ToString. C# numeric conversion supports custom conversion, the ";" character is being used as a section separator. So you can use any other custom formatting, but the example above is the most useful one.
Variables and Expressions Defined at Atom Scope
So far the examples used only global variables. But variables (and expressions) can be also defined per-atom, on the
Atoms-Components-Variables tab. The UI and its behavior is exactly the same as for the global variables, at least in version 3 of the plugin. The main difference is:
- Global expressions can reference only other Global variables
- Atom-scoped expressions can reference either other variables from the same atom, or any Global variables. If any Atom-scoped and a Global variables have the same name, the Atom-scoped one will be used.
There are more plans for Atom-scoped variables for the future, but for now they can be used mostly to organize variables and to remove the clutter at the Global scope if a variable is not needed anywhere outside of an atom.
Atom-scoped variables can still have Public Value source. In that case, the name of the trigger param will include the Atom's name.
Drivers
But on their own, variables and expressions are not of much use. You can use complex expressions to compute magical values, but you then also must be able to use them to do something. That's what
Drivers are for. Basically, they allow you to use any expression to
drive a value of any(*) atom property. Through drivers, you can use the expressions system to change/animate a light's intensity or range, to scale texts on UI elements, to change the value of sliders or toggles, or to change morph values. If you can store it as an atom's state, you can (almost always) also control it by
MATH.
Drivers are added on the
Atoms-Components-Drivers tab. Simply choose a property from the dropdown to add a driver for it. For each component enabled on the atom, it will list all its properties compatible with the Drivers system.
Then type in an expression, and that property's value will be controlled by the expression. That's it! Check
the version 3 update changes for some examples.
It can be somewhat annoying when a driver controls something in the scene at all times. Which is what (by default), drivers are not applied/running when:
- The simulation is paused by the Freeze Motion/Sound VaM's toggle.
- The atom's state has sub-states defined, and you directly apply a specific sub-state. You probably want to edit that sub-state then, without being distracted by drivers animating things. This applies only to that atom of course.
- You apply an atom's transition mid-state.
These are the default settings, you can change them on the
Settings tab.
Per-State Drivers
By default, a driver takes over the value's control regardless of the applied atom's state. Optionally, drivers can be overridden per-state.
- First, tick the Enabled Overrides per State
- Switch to the Atom-States tab and scroll to the corresponding component of the atom. At the bottom of each component with drivers that have overrides enabled, you will see a special section listing those drivers.
- Here you can choose for each driver which expression it should use:
- Parent Expression: This is the default, and if set for all states, will behave as if no overrides were even enabled for the driver:
- For the main state, this will use the driver defined for this property at the component level.
- For a non-main sub-state, if enabled for the state, this will use the main state's driver
- For a transition mid-state, this will use the target state's driver (however, there is no UI where you could edit mid-states directly, yet)
- Custom Expression: Type in an expression specific to this main state, sub-state, or mid-state.
- State Value: Do not use driver at all in this state. Instead, use the value stored for the state, as if it had no driver defined.
As a side note, you can't set State Value directly for the global driver defined for a property at a component level. But using empty expression for it will achieve the same result.
(*) There are some limits to what a driver can control:
- It must be a property of an existing component supported by SceneDirector. Until custom components are implemented (allowing you to build your own component from any atom's triggerable params), what you see in the plugin is what you get. But you can define custom morph components with any morphs, so you can drive those already!
- Version 3 of the plugin supports only scalar variables and expressions. Therefore Drivers can be applied only to booleans, integers, floats or strings. Or in short, if it's a toggle/slider, you can most likely control it by a driver. You can't control colors through drivers (yet)
- The Placement component is rather special and does not expose any of its properties to Drivers, just like you can't control any of them in the UI through sliders, you can only capture the state. There is a plan to expose at least some of it to drivers, once vector and rotation variables are implemented. But possibly only the main controller will be available, I am not sure yet.
Other Use of Variables
For UIObjects, you can use formatted text directly to control the text on UIText, sliders, toggles and other UI objects. By default to avoid confusion, text formatting is not enabled for those fields, the text you type in will be used as is. To enable the formatting for a controlled UI object atom, turn on the toggle
Process Variable references in Text on the
Atoms-Components-General tab.
Examples
Check the
update of version 3 for some examples of how the drivers could be used.
Also as already mentioned, the
DildoLanguage demo scene uses variables and formatted strings to update the button texts, and to show the penetration information box.