EpsilonScript is an interpreter to evaluate a simple expression written in C#.
It targets .NET Standard 2.0.
- Can express simple mathematical expressions
- Can express conditions (i.e. boolean expression)
- Variables with read/write support
- Simple syntax without too much flexibility (yes, this is a feature)
- Supports Unity
- No heap allocation after compilation (with exceptions)
Documentation is still lacking, but the core features are usable. There are not enough tests for some components of the software, which is being worked on now.
Basic arithmetic operators (+
, -
, *
, /
, %
) are supported. Parentheses ((
, )
) are recognized too.
var compiler = new Compiler();
var script = compiler.Compile("(1 + 2 + 3 * 2) * 2", Compiler.Options.Immutable);
script.Execute();
Console.WriteLine(script.IntegerValue);
18
Variables can be read, and also be assigned to (=
). Compound assignment operators can be used too (+=
, -=
, *=
, /=
).
Unique integers generated by GetUniqueIdentifier
extension method are used to address variable names for performance reasons. The resolved identifier integer should be cached for later used.
Variables are stored in an object implementing IVariableContainer
. DictionaryVariableContainer
can be used for the simple implementation.
var compiler = new Compiler();
var valIdentifier = "val".GetUniqueIdentifier();
var variables = new DictionaryVariableContainer {[valIdentifier] = new VariableValue(43.0f)};
var script = compiler.Compile("val = val * 10.0", Compiler.Options.None, variables);
script.Execute();
Console.WriteLine(variables[valIdentifier].FloatValue);
430.0
Compiler.Options.Immutable
flag prevents mutating expression from compiling.
Variables cannot be defined inside the script. This is done on purpose to prevent the expression from doing "too much".
Basic comparison operators are supported (==
, !=
, <
, <=
, >
, >=
) as well as logical operators (!
, &&
, ||
).
var compiler = new Compiler();
var valIdentifier = "val".GetUniqueIdentifier();
var variables = new DictionaryVariableContainer {[valIdentifier] = new VariableValue(43.0f)};
var script = compiler.Compile("val >= 0.0 && val < 50.0", Compiler.Options.Immutable, variables);
script.Execute();
Console.WriteLine(script.BooleanValue);
True
Functions are supported in EpsilonScript. There are built-in functions, but custom functions can be defined too.
var compiler = new Compiler();
compiler.AddCustomFunction(new CustomFunction("rand", (float d) => Random.Range(0.0f, d)));
var script = compiler.Compile("rand(0, 10)", Compiler.Options.Immutable);
script.Execute();
Console.WriteLine(script.FloatValue);
3.1
The list of default built-in functions can be found here.
Functions can be overloaded, which means you can define a function with different signatures (including types and number of parameters) with the same name.
Few built-in functions such as abs
, min
, max
, and ifelse
use this feature.
A custom function can be flagged as a constant function. A constant function must always return the same result given that the input parameters do not change. The execution result of a constant function with constant parameters is cached on a compilation for performance optimization.
A constant function can be created by passing true
to the isConstant
constructor parameter:
new CustomFunction("sin", (float v) => (float) System.Math.Sin(v), true)
For example, a result of the following expression is cached on a compilation, because the built-in sin
function is marked as constant: sin(3.141592 / 2)
Strings can be used, primarily intended to be used for function parameters.
var compiler = new Compiler();
compiler.AddCustomFunction(new CustomFunction("read_save_data", (string flag) => SaveData.Instance.GetIntegerData(flag)));
var script = compiler.Compile(@"read_save_data(""LVL00_PLAYCOUNT"") > 5");
script.Execute();
Console.WriteLine(script.BooleanValue);
If SaveData.Instance.GetIntegerData("LVL00_PLAYCOUNT")
returns 10:
True
Strings can be concatenated with strings.
"Hello " + "World"
"Hello World"
Strings can also be concatenated with numbers.
"Debug: " + 128
"Debug: 128"
Strings can be compared with strings.
"Hello" == "Hello"
true
Heap (GC) allocations are typically a concern for games.
EpsilonScript avoids GC allocations after compilation. However, there are a few exceptions where allocations must happen.
Due to how C# works, concatenating strings will result in heap allocations.
"Debug: " + i
where i
is a variable.
Please note that constant string concatenation is done on a compilation, and won't produce any garbage when executed.
"Debug: " + 42 * 42
If user-defined custom functions cause heap allocations, calling that function from a script will produce garbage.
In many games, game designers often want to express some kind of condition. For example, there may be a scenario where a character should act differently depending on what the player has done.
monsters_fought == 0 && has_key
It is common to use a scripting engine like Lua to let the game designers write the game logic for maximum freedom, but that is not always feasible. Many games require frequent content updates after the first release and are typically maintained for many years with minimum software regression.
Data-driven approach (e.g. Microsoft Excel, Google Spreadsheet, Unity serialization, etc) is often chosen, which trades freedom with a more controlled environment, but entering complex expression can become cumbersome or even impossible. EpsilonScript is one of my attempts to empower the game designers with a strength of expression in such an environment.
On a different note, an expression can also be difficult to express with node-based visual scripting. A complex expression often requires multiple nodes to implement, and are difficult to intuitively understand what they do. Being able to simply write an expression in text is much faster and more readable.
Finally, features that are hard to read, or can cause complication is avoided on purpose. For example, the ternary operator is not implemented (built-in IfElse
function can be used to achieve the same result with clearer syntax).