项目作者: aki-null

项目描述 :
Typed math expression with variable assignment support for inline scripting
高级语言: C#
项目地址: git://github.com/aki-null/epsilon-script.git
创建时间: 2020-01-20T07:01:05Z
项目社区:https://github.com/aki-null/epsilon-script

开源协议:MIT License

下载


EpsilonScript

EpsilonScript is an interpreter to evaluate a simple expression written in C#.

It targets .NET Standard 2.0.

Features

  • 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)

Project State

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.

Samples

Arithmetics

Basic arithmetic operators (+, -, *, /, %) are supported. Parentheses ((, )) are recognized too.

Code

  1. var compiler = new Compiler();
  2. var script = compiler.Compile("(1 + 2 + 3 * 2) * 2", Compiler.Options.Immutable);
  3. script.Execute();
  4. Console.WriteLine(script.IntegerValue);

Result

  1. 18

Variables

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.

Code

  1. var compiler = new Compiler();
  2. var valIdentifier = "val".GetUniqueIdentifier();
  3. var variables = new DictionaryVariableContainer {[valIdentifier] = new VariableValue(43.0f)};
  4. var script = compiler.Compile("val = val * 10.0", Compiler.Options.None, variables);
  5. script.Execute();
  6. Console.WriteLine(variables[valIdentifier].FloatValue);

Result

  1. 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”.

Comparison

Basic comparison operators are supported (==, !=, <, <=, >, >=) as well as logical operators (!, &&, ||).

Code

  1. var compiler = new Compiler();
  2. var valIdentifier = "val".GetUniqueIdentifier();
  3. var variables = new DictionaryVariableContainer {[valIdentifier] = new VariableValue(43.0f)};
  4. var script = compiler.Compile("val >= 0.0 && val < 50.0", Compiler.Options.Immutable, variables);
  5. script.Execute();
  6. Console.WriteLine(script.BooleanValue);

Result

  1. True

Functions

Functions are supported in EpsilonScript. There are built-in functions, but custom functions can be defined too.

Code

  1. var compiler = new Compiler();
  2. compiler.AddCustomFunction(new CustomFunction("rand", (float d) => Random.Range(0.0f, d)));
  3. var script = compiler.Compile("rand(0, 10)", Compiler.Options.Immutable);
  4. script.Execute();
  5. Console.WriteLine(script.FloatValue);

Result

  1. 3.1

The list of default built-in functions can be found here.

Overloading

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.

Constant Functions

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:

  1. 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)

String

Strings can be used, primarily intended to be used for function parameters.

Code

  1. var compiler = new Compiler();
  2. compiler.AddCustomFunction(new CustomFunction("read_save_data", (string flag) => SaveData.Instance.GetIntegerData(flag)));
  3. var script = compiler.Compile(@"read_save_data(""LVL00_PLAYCOUNT"") > 5");
  4. script.Execute();
  5. Console.WriteLine(script.BooleanValue);

Result

If SaveData.Instance.GetIntegerData("LVL00_PLAYCOUNT") returns 10:

  1. True

Strings can be concatenated with strings.

Code

  1. "Hello " + "World"

Result

  1. "Hello World"

Strings can also be concatenated with numbers.

Code

  1. "Debug: " + 128

Result

  1. "Debug: 128"

Strings can be compared with strings.

Code

  1. "Hello" == "Hello"

Result

  1. true

On Heap Allocations

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.

String concatenations

Due to how C# works, concatenating strings will result in heap allocations.

  1. "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.

  1. "Debug: " + 42 * 42

User Defined Custom Functions

If user-defined custom functions cause heap allocations, calling that function from a script will produce garbage.

Use Cases

Condition Validator

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.

  1. monsters_fought == 0 && has_key

Context

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).