Flecs Script is a runtime interpreted DSL for creating entities and components that is optimized for defining scenes, assets and configuration. In a nutshell, Flecs Script is to ECS what HTML/JSX is to a browser.
Some of the features of Flecs Script are:
- Native support for named entities, hierarchies and inheritance
- Assign component values
- Expressions and variables (
var + 10) - Conditionals and loops (
if var > 10,for i in [0..10]) - Native integration with templates (procedural assets)
struct MaxSpeed(value: f32)
struct Position(x: f32, y: f32)
prefab SpaceShip {
MaxSpeed: {value: 100}
cockpit {
Position: {x: -10, y: 0}
}
}
my_spaceship : SpaceShip {
Position: {x: 10, y: 20}
}This section goes over the basic syntax over Flecs Script.
An entity is created by specifying an identifier followed by a scope. Example:
my_entity {}An entity scope can contain components and child entities. The following example shows how to add a child entity:
my_parent {
my_child {}
}Note how a scope is also added to the child entity.
To create anonymous entities, leave out the entity name:
{
my_child {} // named child with anonymous parent
}Alternatively, the _ placeholder can be used to indicate an anomyous entity:
_ {
my_child {} // named child with anonymous parent
}The _ placeholder can be useful in combination with syntax constructs that require an identifier token, such as inheritance:
// anonymous entity that inherits from SpaceShip
_ : SpaceShip { }Entity names can be specified using a string. This allows for entities with names that contain special characters, like spaces:
"my parent" {
"my child" {}
}String names can be combined with string interpolation (see below) to create names that are computed when the script is evaluated:
"USS_$name" {}By default children are created using the ChildOf hierarchy storage. To select the Parent hierarchy storage, add the tree annotation:
@tree Parent
my_parent {
my_child {}
}A tag can be added to an entity by simply specifying the tag's identifier in an entity scope. Example:
SpaceShip {} // Define SpaceShip tag
my_entity {
SpaceShip // Add SpaceShip to my_entity
}Pairs are added to entities by adding them to an entity scope, just like tags:
Likes {}
Pizza {}
my_entity {
(Likes, Pizza)
}Components are specified like tags, but with an additional value:
my_entity {
Position: {x: 10, y: 20}
}For a component to be assignable with a value, it also needs to be described in the reflection framework.
A component can also be added without a value. This will create a default constructed component. Example:
my_entity {
Position
}The value after the : is an expression. For components that hold a single value, such as a scalar type, the value can be assigned directly without curly braces:
my_entity {
Mass: 100
Mass: 50 + 50
Mass: $weight
}Components can be defined in a script (see Type definitions):
struct Position(x: f32, y: f32)
my_entity {
Position: {x: 10, y: 20}
}Components can be pairs:
my_entity {
(Start, Position): {x: 0, y: 0}
(Stop, Position): {x: 10, y: 20}
}When referring to child entities or components, identifiers need to include the parent path as well as the entity name. Paths are provided as lists of identifiers separated by a dot (.):
Sun {
Earth {
solarsystem.Planet
}
}Paths can only be used to refer to existing entities. The name of an entity that's created by a script cannot be a path:
// Invalid, entity names cannot be paths
Sun.Earth {
solarsystem.Planet
}To avoid having to repeatedly type the same paths, use the using statement (see below).
To create a singleton component, use $ as the entity identifier:
$ {
TimeOfDay: { t: 0.5 }
}Multiple singleton components can be specified in the same scope:
$ {
TimeOfDay: { t: 0.5 }
Player: { name: "bob" }
}
An entity can be created with a "kind", which is a component specified before the entity name. This is similar to adding a tag or component in a scope, but can provide a more natural way to describe things. For example:
SpaceShip my_spaceship {}This is equivalent to doing:
my_spaceship {
SpaceShip
}When using the entity kind syntax, the scope is optional:
SpaceShip my_spaceship // no {}If the specified kind is a component, a value can be specified between parentheses:
CheckBox my_checkbox(checked: true)When the entity kind is a component, a value will always be assigned even if none is specified. This is different from component assignments in a scope. Example:
CheckBox my_checkbox(checked: true)
// is equivalent to
my_checkbox {
CheckBox: {checked: true}
}CheckBox my_checkbox
// is equivalent to
my_checkbox {
CheckBox: {}
}Applications can specify the following builtin kinds which provide convenience shortcuts to commonly used features:
prefab SpaceShip
// is equivalent to
Prefab spaceshipScripts can natively specify inheritance relationships between entities, which is useful in particular for prefabs. Example:
prefab SpaceShip {
MaxSpeed: {value: 100}
}
my_spaceship : SpaceShip {}The : notation is short for adding an IsA relationship with the relationship syntax:
my_spaceship {
(IsA, SpaceShip)
}By default entity hierarchies are created with the ChildOf relationship. Other relationships can also be used to create hierarchies by combining a pair with a scope. Example:
(IsA, Thing) {
(IsA, Organism) {
(IsA, Plant) {
Tree {}
}
(IsA, Animal) {
Human {}
}
}
}Scripts can contain expressions, which allow for computing values from inputs such as component values, template properties and variables. Here are some examples of valid Flecs script expressions:
const x = 10 + 20 * 30
const x = 10 * (20 + 30)
const x = $var * 10
const x = pow($var, 2)
const x = e.parent().name()
const x: Position = {10, 20}
const x: Position = {x: 10, y: 20}The following sections describe the different features of expressions.
The following operators are supported in expressions, in order of precedence:
| Symbol | Description | Example |
|---|---|---|
! |
Logical NOT | !10 |
* |
Multiplication | 10 * 20 |
/ |
Division | 10 / 20 |
% |
Modulus | 10 % 3 |
+ |
Addition | 10 + 20 |
- |
Subtraction/negative | 10 - 20, -(10, 20) |
<< |
Bitwise left shift | 10 << 1 |
>> |
Bitwise right shift | 10 >> 1 |
> |
Greater than | 10 > 20 |
>= |
Greater than or equal | 10 >= 20 |
< |
Less than | 10 < 20 |
<= |
Less than or equal | 10 <= 20 |
== |
Equality | 10 == 20 |
!= |
Not equal | 10 != 20 |
& |
Bitwise AND | 2 & 6 |
| |
Bitwise OR | 2 | 4 |
&& |
Logical AND | true && false |
|| |
Logical OR | true || false |
The following table lists the different kinds of values that are supported in expressions:
| Value kind | Type | Example |
|---|---|---|
| Integer | i64 |
42, -100, 0, 0x1A |
| Floating Point | f64 |
3.14, -2.718, 1e6, 0.0 |
| String | string |
"Hello, World!", "123", "" |
| Multiline string | string |
`Hello World` |
| Entity | entity |
spaceship, spaceship.pilot |
| Enum/Bitmask values | from lvalue | Red, Blue, Lettuce | Bacon |
| Composites | from lvalue | {x: 10, y: 20}, {10, 20} |
| Collections | from lvalue | [1, 2, 3] |
Initializers are values that are used to initialize composite and collection members. Composite values are initialized by initializers that are delimited by {}, while collection initializers are delimited by []. Furthermore, composite initializers can specify which member of the composite value should be initialized. Here are some examples of initializer expressions:
{}
{10, 20}
{x: 10, y: 20}
{{10, 20}, {30, 40}}
{start: {x: 10, y: 20}, stop: {x: 10, y: 20}}
[10, 20, 30]
[{10, 20}, {30, 40}, {50, 60}]
{x += 10, y *= 2}Composite initializers must always be assigned to an lvalue of a well defined type. This can either be a typed variable, component assignment, function parameter or in the case of nested initializers, an element of another initializer. For example, this is a valid usage of an initializer:
const x: Position = {10, 20}while this is an invalid usage of an initializer:
// Invalid, unknown type for initializer
const x = {10, 20}Collection initializers do not require a well defined type (see vector literals).
When a collection initializer is not assigned to an lvalue of a well defined type, it evaluates to a vector. The element type of the vector is derived from the initializer elements, where the most expressive element type determines the vector type:
const a = [10, 20, 30] // vector<i64>
const b = [10, 10.5, 20] // vector<f64>
const c = ["foo", "bar"] // vector<string>Element types that cannot be implicitly converted to each other, such as numbers and strings, cannot be mixed in the same vector literal.
Ranges can also be assigned, in which case they materialize into a vector with the values in the range (the end of the range is exclusive):
const v = [1 .. 5] // vector<i32> [1, 2, 3, 4]When assigning variables to elements in a composite initializer, applications can use the following shorthand notation if the variable names are the same as the member name of the element:
// Normal notation
Tree: {color: $color, height: $height}
// Shorthand notation
Tree: {color: $, height: $}Initializer expressions may contain add assignment (+=) or multiply assignment (*=) operators. These operators allow an initializer to modify an existing value. An example:
e {
Position: {10, 20}
Position: {x += 1, y += 2}
}
// e will have Position{11, 22}This can be especially useful when used in combination with templates (see below):
template Tree {
prop height: f32 = 4
// Make sure tree doesn't sink through the ground
Position: {y += height}
}
e {
Position: {10, 0}
Tree: {height: 3}
}
// e will have Position{10, 3}Match expressions can be used to conditionally assign a value. An example:
const x = 1
// y will be assigned with value 10
const y = match x {
1: 10
2: 20
3: 30
}The input to a match expression must be matched by one of its cases. If the input is not matched, script execution will fail. Match expressions can include an "any" case, which is selected when none of the other cases match:
const x = 4
// y will be assigned with value 100
const y = match x {
1: 10
2: 20
3: 30
_: 100
}Match expressions can be used to assign components:
e {
Position: match i {
1: {10, 20}
2: {20, 30}
3: {40, 50}
}
}A case is terminated by a newline, a ;, or the closing } of the match expression. This makes it possible to write a match expression on a single line, as long as every case that is not the last one is terminated with a ;:
const y = match x { 1: 10; 2: 20; 3: 30 }The type of a match expression is derived from the case values. When the case statements in a match contain values of multiple types, the most expressive type is selected. The algorithm for determining the most expressive type is the same as the one used to determine the type for binary expressions. When a match expression contains values with conflicting types, script execution will fail.
A new expression is the new keyword followed by an entity statement. New expressions can be used to create entities inside of expressions. The following are examples of valid new expressions:
// Create a new anonymous entity, assign to variable x
const x = new {}
// Create a new anonymous entity with Position component, assign to variable x
const x = new {
Position: {10, 20}
}
// Create a new entity with name Foo and Position component, assign to variable x
const x = new Foo {
Position: {10, 20}
}New expressions can be used anywhere where an expression of an entity type is expected. The following example shows how to use a new expression inside of an initializer:
// Create entity with TrafficLight component which has red, orange and green
// members of type entity.
e {
TrafficLight: {
red: new { Color: {255, 0, 0} }
orange: new { Color: {255, 128, 0} }
green: new { Color: {0, 255, 0} }
}
}The behavior of new expressions is exactly the same as entity statements in that they respect the context in which they are used, such as the current hierarchy scope and with statements:
some_parent {
// Create new anonymous child of some_parent with Position component, assign
// to variable x
const x = new { Position: {10, 20} }
}
with Position(10, 20) {
// Create new anonymous entity with Position: {10, 20}
const x = new { }
}All features that are supported by entity statements are also available for new expressions, such as the ability to have children:
const x = new {
Positiion: {10, 20}
// Child of anonymous entity created by new expression
child {
Position: {20, 30}
}
}The primary use case for new expressions is to make it possible to create anonymous entities that can be referred to afterwards by a script. Without new expressions this is not possible, as illustrated here:
// Create anonymous entities
{
Color: {255, 0, 0}
}
{
Color: {255, 128, 0}
}
{
Color: {0, 255, 0}
}
e {
TrafficLight: {
// Can't refer to anonymous entities here
red: // ???
orange: // ???
green: // ???
}
}Without new expressions the only workaround is to use named entities, but this introduces overhead and increases memory footprint. With new expressions the example can be expressed with just anonymous entities:
// Create anonymous entities, store in variables
const red = new {
Color: {255, 0, 0}
}
const orange = new {
Color: {255, 128, 0}
}
const green = new {
Color: {0, 255, 0}
}
e {
TrafficLight: {
// Assign variables to members
red: red
orange: orange
green: green
}
}A new expression may only create a single entity.
Flecs script supports interpolated strings, which are strings that can contain expressions. String interpolation supports two forms, where one allows for easy embedding of variables, whereas the other allows for embedding any kind of expression. The following example shows an embedded variable:
const x = "The value of PI is $PI"The following example shows how to use an expression:
const x = "The circumference of the circle is {2 * $PI * $r}"To prevent evaluating expressions in an interpolated string, the $ and { characters can be escaped:
const x = "The value of variable \$x is $x"Interpolated f32 and f64 values can include a format specifier after the
expression, separated by a colon:
const value = 12.3456
const x = "{value:.2}" // 12.35The complete syntax is:
{expression:[[fill]align][+][0][width][.precision][e|E]}
| Part | Description |
|---|---|
fill |
Character used for padding. It must be immediately followed by an alignment character. The default is a space. |
< |
Align the value to the left. |
^ |
Center the value. |
> |
Align the value to the right. This is the default. |
+ |
Always include a sign, including for positive values. |
0 |
Pad numeric values with leading zeroes. The sign, when present, is placed before the zeroes. |
width |
Minimum width of the formatted value. Values wider than this are not truncated. |
.precision |
Number of digits after the decimal point. |
e |
Use scientific notation with a lowercase exponent. |
E |
Use scientific notation with an uppercase exponent. |
For example:
const value = 12.5
const left = "{value:*<13}" // 12.500000****
const center = "{value:*^13}" // **12.500000**
const right = "{value:*>13}" // ****12.500000
const zeroes = "{value:013}" // 000012.500000
const sign = "{value:+}" // +12.500000
const exp = "{value:.2e}" // 1.25e+01Width and precision can be integer literals, variables, or parenthesized
expressions. Variable names can be written with or without $:
const value = 12.3456
const width = 10
const precision = 2
const a = "{value:width}"
const b = "{value:$width}"
const c = "{value:(width + 2)}"
const d = "{value:.precision}"
const e = "{value:.$precision}"
const f = "{value:.(precision + 1)}"Width and precision values must be between 0 and 1024, inclusive. Values
outside this range produce an error.
The type of an expression is determined by the kind of expression, its operands and the context in which the expression is evaluated. The words "type" and "component" can be used interchangeably, as every type in Flecs is a component, and every component is a type. For component types to be used with scripts, they have to be described using the meta reflection addon.
The following sections go over the different kinds of expressions and how their types are derived.
Unary expressions have a single operand, with the operator preceding it. The following table shows the different unary operators with the expression type:
| Operator | Expression Type |
|---|---|
! |
bool |
- |
Same as operand. |
Binary expressions have two operands. The following table shows the different binary operators with the expression type. The operand type is the type to which the operands must be castable for it to be a valid expression.
| Symbol | Expression type | Operand type |
|---|---|---|
* |
other (see below) | Numbers |
/ |
f64 |
Numbers |
+ |
other (see below) | Numbers |
- |
other (see below) | Numbers |
% |
i64 |
i64 |
<< |
other (see below) | Integers |
>> |
other (see below) | Integers |
> |
bool |
Numbers |
>= |
bool |
Numbers |
< |
bool |
Numbers |
<= |
bool |
Numbers |
== |
bool |
Values |
!= |
bool |
Values |
& |
other (see below) | Integers |
| |
other (see below) | Integers |
&& |
bool |
bool |
|| |
bool |
bool |
For the operators where the expression type is listed as "other" the type is derived by going through these steps:
- If the types of the operands are equal, the expression type will be the operand type.
- If the types are different:
- For literal values, find the smallest storage type without losing precision. If operand types are now equal, use that.
- Find the most expressive type of the two operands (see below)
- If a cast to the most expressive type does not result in a loss of precision, use that.
- If the types are both numbers follow these rules in order:
- If one of the types is a floating point, use
f64 - If one of the types is an integer, use
i64 - If neither, throw a type incompatible error
- If one of the types is a floating point, use
For equality expressions (using the == or != operators), additional rules are used:
- If one of the operands is a bool, cast the other operand to a bool as well. This ensures that expressions such as
2 == trueevaluate to true. - If one of the operands is a floating point value and the other operand is a literal, the literal is cast to the floating point type of the other operand. This ensures that expressions such as
f32_value == 0.1evaluate to true. - Equality expressions where both operands are floating point literals are invalid, as comparing two floating point literals for equality is usually a mistake.
Type expressiveness is determined by the kind of type and its storage size. The following tables show the expressiveness and storage scores:
| Type | Expressiveness Score |
|---|---|
| bool | 1 |
| char | 2 |
| u8 | 2 |
| u16 | 3 |
| u32 | 4 |
| uptr | 5 |
| u64 | 6 |
| i8 | 7 |
| i16 | 8 |
| i32 | 9 |
| iptr | 10 |
| i64 | 11 |
| f32 | 12 |
| f64 | 13 |
| string | -1 |
| entity | -1 |
| Type | Storage Score |
|---|---|
| bool | 1 |
| char | 1 |
| u8 | 2 |
| u16 | 3 |
| u32 | 4 |
| uptr | 6 |
| u64 | 7 |
| i8 | 1 |
| i16 | 2 |
| i32 | 3 |
| iptr | 5 |
| i64 | 6 |
| f32 | 3 |
| f64 | 4 |
| string | -1 |
| entity | -1 |
The function to determine whether a type is implicitly castable is:
bool implicit_cast_allowed(from, to) {
if (expressiveness(to) >= expressiveness(from)) {
return storage(to) >= storage(from);
} else {
return false;
}
}If either the expressiveness or storage scores are negative, the operand types are not implicitly castable.
If the left operand of a binary expression is of a vector type, the operation will be executed for each of its operands. A vector type is a type that meets the following criteria:
- The type must be a primitive or struct type.
- If the type is a struct type:
- All members must be of the same type.
- The member type must be primitive.
For example:
// Valid vector type: all members are of the same primitive type
struct Position {
float x;
float y;
float z;
};
// Not a valid vector type: members are not of a primitive type
struct Line {
Position start;
Position stop;
};
// Not a valid vector type: not all members are of the same type
struct Rgba {
int8_t r;
int8_t g;
int8_t b;
float a;
};An example of a vector operation:
const p0: Position = {10, 20, 30}
const p1 = p0 + 1 // {11, 21, 31}When a member is accessed on a vector type whose members all have single-letter names, and the accessed member cannot be resolved to an existing member, the accessor is interpreted as a swizzle. A swizzle builds a new value from the members that match its letters, in the order they are specified. The result obtains the type of the lvalue it is assigned to.
The members of a swizzle may appear in any order, and may be repeated. For a type with members r, g, b, the swizzles rgb, bgr, rrr and bb are all valid.
For example:
const p: Position = {10, 20, 30}
e {
// Swizzle desugars to {p.z, p.y, p.x}
Velocity: p.zyx // {30, 20, 10}
}Lvalues are the left side of assignments. There are two kinds of assignments possible in Flecs script:
- Variable initialization
- Initializer initialization
The type of an expression can be influenced by the type of the lvalue it is assigned to. For example, if the lvalue is a variable of type Position, the assigned initializer will also be of type Position:
const p: Position = {10, 20}Similarly, when an initializer is used inside of an initializer, it obtains the type of the initializer element. In the following example the outer initializer is of type Line, while the inner initializers are of type Point:
const l: Line = {{10, 20}, {30, 40}}Another notable example where this matters is for enum and bitmask constants. Consider the following example:
const c: Color = RedHere, Red is a resolvable identifier, even though the fully qualified identifier is Color.Red. However, because the type of the lvalue is of enum type Color, the expression Red will be resolved in the scope of Color.
Expressions can call functions. Functions in Flecs script can have arguments of any type, and must return a value. The following snippet shows examples of function calls:
const x = sqrt(100)
const x = pow(100, 2)
const x = add({10, 20}, {30, 40})Functions can be defined in scripts or by using the C/C++ API. Flecs also comes with a set of builtin functions for common math utilities and functions that provide access to ECS features. Math functions are defined by the script math addon, which must be explicitly enabled by defining FLECS_SCRIPT_MATH.
A function can be created in code by doing:
ecs_function(world, {
.name = "sum",
.return_type = ecs_id(ecs_i64_t),
.params = {
{ .name = "a", .type = ecs_id(ecs_i64_t) },
{ .name = "b", .type = ecs_id(ecs_i64_t) }
},
.callback = sum
});Function implementations looks like this:
void sum(
const ecs_function_ctx_t *ctx,
int32_t argc,
const ecs_value_t *argv,
ecs_value_t *result)
{
int64_t *a = argv[0].ptr;
int64_t *b = argv[1].ptr;
*(int64_t*)result->ptr = *a + *b;
}The following syntax can be used to define a function in a script:
fn add(a: i32, b: i32) -> i32 {
a + b // last expression is return value
}
Foo { Position: {add(1, 2), add(10, 20)} }Script functions are created and called in the same way as functions created with the API.
Function bodies may only contain expressions and const variables, for example:
fn poly(x: i32) -> i32 {
const x2: i32 = x * x
const x3: i32 = x2 * x
x3 + x2
}Control flow statement such as if and for are not allowed inside of a function. To expression conditional logic, functions can use match expressions:
fn factorial(n: i32) -> i32 {
match n {
0: 1
_: factorial(n - 1) * n
}
}Methods are functions that are called on instances of the method's type. The first argument of a method is the instance on which the method is called. The following snippet shows examples of method calls:
const x = v.length()
const x = v1.add(v2)Just like functions, methods can currently only be defined outside of scripts by using the Flecs Script API.
A method can be created in code by doing:
ecs_method(world, {
.name = "add",
.parent = ecs_id(ecs_i64_t), // Add method to i64
.return_type = ecs_id(ecs_i64_t),
.params = {
{ .name = "a", .type = ecs_id(ecs_i64_t) }
},
.callback = sum
});Vector functions are functions that accept arguments of a builtin ScriptVectorType type. This allows these functions to accept any type that is a valid vector type (see Vector operations).
Here is a usage example of a vector function:
const red: Rgb = {255, 0, 0}
const blue: Rgb = {0, 0, 255}
const purple = lerp(red, blue, 0.5)When a vector function is called, all of the arguments provided to parameters of ScriptVectorType must be of the same type. The following code is therefore not valid:
const red: Rgb = {255, 0, 0}
const p: Position = {10, 20, 30}
const red_p = lerp(red, p, 0.5) // Illegal: red and p are of different typesVector functions are registered like normal functions, but instead of specifying a callback, the application sets vector_callbacks. An example:
ecs_function(world, {
.name = "lerp",
.return_type = EcsScriptVectorType,
.params = {
{ "a", EcsScriptVectorType },
{ "b", EcsScriptVectorType },
{ "t", ecs_id(ecs_f64_t) },
},
.vector_callbacks = {
[EcsF32] = lerp_f32,
[EcsF64] = lerp_f64
}
});The signature for vector functions accepts an additional argument for the number of elements in the vector type:
void lerp_f32(
const ecs_function_ctx_t *ctx,
int32_t argc,
const ecs_value_t *argv,
ecs_value_t *result,
int32_t elem_count)
{
float *a = argv[0].ptr;
float *b = argv[1].ptr;
double t = *(double*)argv[2].ptr;
float *r = result->ptr;
for (int i = 0; i < elem_count; i ++) {
r[i] = a[i] + t * (b[i] - a[i]);
}
}In the function documentation below the type of vector parameters is written as [].
The following table lists builtin core functions in the flecs.script.core namespace:
| Function Name | Description | Return Type | Arguments |
|---|---|---|---|
pair |
Returns a pair identifier | id |
(entity, entity) |
The following table lists builtin methods on the flecs.meta.entity type:
| Method Name | Description | Return Type | Arguments |
|---|---|---|---|
name |
Returns entity name | string |
() |
path |
Returns entity path | string |
() |
parent |
Returns entity parent | entity |
() |
has |
Returns whether entity has component | bool |
(id) |
The following table lists doc methods on the flecs.meta.entity type:
| Method Name | Description | Return Type | Arguments |
|---|---|---|---|
doc_name |
Returns entity doc name | string |
() |
doc_uuid |
Returns entity doc uuid | string |
() |
doc_brief |
Returns entity doc brief description | string |
() |
doc_detail |
Returns entity doc detailed description | string |
() |
doc_link |
Returns entity doc link | string |
() |
doc_color |
Returns entity doc color | string |
() |
To use the doc functions, make sure to use a Flecs build compiled with FLECS_DOC (enabled by default).
The following table lists math functions in the flecs.script.math namespace:
| Function Name | Description | Return Type | Arguments |
|---|---|---|---|
cos |
Compute cosine | f64 |
(f64) |
sin |
Compute sine | f64 |
(f64) |
tan |
Compute tangent | f64 |
(f64) |
acos |
Compute arc cosine | f64 |
(f64) |
asin |
Compute arc sine | f64 |
(f64) |
atan |
Compute arc tangent | f64 |
(f64) |
atan2 |
Compute arc tangent with two parameters | f64 |
(f64, f64) |
cosh |
Compute hyperbolic cosine | f64 |
(f64) |
sinh |
Compute hyperbolic sine | f64 |
(f64) |
tanh |
Compute hyperbolic tangent | f64 |
(f64) |
acosh |
Compute area hyperbolic cosine | f64 |
(f64) |
asinh |
Compute area hyperbolic sine | f64 |
(f64) |
atanh |
Compute area hyperbolic tangent | f64 |
(f64) |
exp |
Compute exponential function | f64 |
(f64) |
ldexp |
Generate value from significant and exponent | f64 |
(f64, f32) |
log |
Compute natural logarithm | f64 |
(f64) |
log10 |
Compute common logarithm | f64 |
(f64) |
exp2 |
Compute binary exponential function | f64 |
(f64) |
log2 |
Compute binary logarithm | f64 |
(f64) |
pow |
Raise to power | f64 |
(f64, f64) |
sqrt |
Compute square root | f64 |
(f64) |
sqr |
Compute square | f64 |
(f64) |
ceil |
Round up value | f64 |
(f64) |
floor |
Round down value | f64 |
(f64) |
round |
Round to nearest | f64 |
(f64) |
abs |
Compute absolute value | f64 |
(f64) |
min |
Return smallest of two values | f64 |
(f64, f64) |
max |
Return largest of two values | f64 |
(f64, f64) |
clamp |
Clamp value between minimum/maximum | [] |
([] v, [] min, f64 max) |
lerp |
Interpolate between two values | [] |
([] a, [] b, f64 t) |
smoothstep |
Smooth interpolation between two values | [] |
([] a, [] b, f64 t) |
dot |
Return dot product for two vectors | f64 |
([] a, [] b) |
length |
Return length of vector | f64 |
([] v) |
length_sq |
Return squared length of vector | f64 |
([] v) |
normalize |
Normalize vector | [] |
([] v) |
perlin2 |
2D perlin noise function | f64 |
(f64 x, f64 y) |
The following table lists the constants in the flecs.script.math namespace:
| Function Name | Description | Type | Value |
|---|---|---|---|
E |
Euler's number | f64 |
2.71828182845904523536028747135266250 |
PI |
Ratio of circle circumference to diameter | f64 |
3.14159265358979323846264338327950288 |
The following table lists methods of the flecs.script.math.Rng type:
| Method Name | Description | Return Type | Arguments |
|---|---|---|---|
u |
Returns random unsigned integer between 0 and max | u64 |
(u64 max) |
f |
Returns random floating point between 0 and max | f64 |
(f64 max) |
The random number generator can be used like this:
const rng: flecs.script.math.Rng = {}
const x = $rng.f(1.0)To use the math functions, make sure to use a Flecs build compiled with the FLECS_SCRIPT_MATH addon (disabled by default) and that the module is imported:
ECS_IMPORT(world, FlecsScriptMath);The script platform addon exposes constants in the flecs.script.platform namespace that describe the operating system and compiler that the application was built with. This makes it possible to write scripts that conditionally load assets or configuration based on the platform.
The following table lists the string constants in the flecs.script.platform namespace:
| Constant Name | Description | Type | Possible Values |
|---|---|---|---|
os |
Operating system the build targets | string |
windows, android, linux, freebsd, darwin, emscripten, unknown |
compiler |
Compiler the build was compiled with | string |
msvc, clang, mingw, gcc, unknown |
The following table lists the boolean constants in the flecs.script.platform namespace. A constant is true when the application was built for that platform or compiler, and false otherwise:
| Constant Name | Description | Type |
|---|---|---|
WINDOWS |
Whether the build targets Windows | bool |
POSIX |
Whether the build targets a POSIX system | bool |
ANDROID |
Whether the build targets Android | bool |
LINUX |
Whether the build targets Linux | bool |
FREEBSD |
Whether the build targets FreeBSD | bool |
DARWIN |
Whether the build targets macOS/iOS | bool |
EMSCRIPTEN |
Whether the build targets Emscripten | bool |
MINGW |
Whether the build was compiled with MinGW | bool |
GNU |
Whether the build was compiled with GCC | bool |
The platform constants can be used like this:
using flecs.script
const platform_name = platform.os
if platform.WINDOWS {
// ...
}To use the platform constants, make sure to use a Flecs build compiled with the FLECS_SCRIPT_PLATFORM addon (disabled by default) and that the module is imported:
ECS_IMPORT(world, FlecsScriptPlatform);Templates are parameterized scripts that can be used to create procedural assets. Templates can be created with the template keyword. Example:
template Square {
Color: {255, 0, 0}
Rectangle: {width: 100, height: 100}
}The script contents of an template are not ran immediately. Instead they are ran whenever an template is instantiated. To instantiate an template, add it as a regular component to an entity:
my_entity {
Square
}
// is equivalent to
my_entity {
Color: {255, 0, 0}
Rectangle: {width: 100, height: 100}
}Templates are commonly used in combination with the kind syntax:
Square my_entityTemplates can be parameterized with properties. Properties are variables that are exposed as component members. When the component is updated with a new value, the template is reevaluated. To create a property, use the prop keyword. Example:
template Square {
prop size = 10
prop color: Color = {255, 0, 0}
Color: $color
Rectangle: {width: size, height: size}
}
Square my_entity(size: 20, color: {38, 25, 13})Just like const variables, prop variables can explicitly specify a type or implicitly derive their type from the assigned (default) value.
An explicitly typed property can omit its default value, as in prop color: Color. The property is then initialized with the type's default constructor. This is only supported for properties; const and mut variables require an initializer.
In addition to property variables, templates can also contain mutables. Mutables that are exposed as component members on a TemplateComponent::mut component. To create a mutable, use the mut keyword. For example the hover mutable variable ends up on a Button::mut component:
template Button {
prop text = "Howdy"
mut hover = false
// ...
}Template scripts can do anything a regular script can do, including creating child entities. The following example shows how to create an template that uses a nested template to create children:
template Tree {
prop height = 10
const wood_color: Color = {38, 25, 13}
const leaves_color: Color = {51, 76, 38}
const canopy_height = 2
const trunk_height = $height - $canopy_height
const trunk_width = 2
Trunk {
Position: {0, ($height / 2), 0}
Rectangle: {$trunk_width, $trunk_height}
Color: $wood_color
}
Canopy {
const canopy_y = $trunk_height + ($canopy_height / 2)
Position3: {0, $canopy_y, 0}
Box: {$canopy_width, $canopy_height}
Color: $leaves_color
}
}
template Forest {
Tree(height: 5) {
Position: {x: -10}
}
Tree(height: 10) {
Position: {x: 0}
}
Tree(height: 7) {
Position: {x: 10}
}
}
Forest my_forestTemplates are structs, where each property is a struct member. This means that a template can inherit from a struct or from another template by specifying a base type after the template name:
template Shape {
prop color: Color = {255, 0, 0}
}
template Square : Shape {
prop size = 10
Color: $color
Rectangle: {width: size, height: size}
}
Square my_square(color: {0, 255, 0}, size: 20)A derived template inherits the properties of its base, including their default values. Inherited properties can be used in the template body just like the template's own properties. Only the properties are inherited: the script contents of the base template are not evaluated when the derived template is instantiated.
The base must be defined before the derived template, and a derived template cannot redefine an inherited property.
A property can be declared with the type of another template by using the template keyword. This makes it possible to configure a template with the properties of another template:
template Leaf {
prop color: Color = {51, 76, 38}
prop size = 1
Color: $color
Rectangle: {width: size, height: size}
}
template Tree {
prop leaf : template Leaf
Leaf1 { leaf }
Leaf2 { leaf: {size: 2} }
}
Tree my_tree(leaf: {color: {255, 0, 0}, size: 3})Inside the template body the property can be used as if it is the template component itself, with or without the $ prefix. When the property is used without an initializer, the component is set to the value of the property. When the property is used with an initializer, members that are not specified in the initializer keep the value of the property. In the above example Leaf2 gets a red leaf of size 2.
A template property can also be used in with statements:
template Tree {
prop leaf : template Leaf
with leaf {
Leaf1 {}
Leaf2 {}
}
with leaf(size: 2) {
Leaf3 {}
}
}A template property without a default value is initialized with the default values of the properties of its template. Like other properties, template properties are exposed as struct members, which means they can be accessed in expressions ($leaf.size) and can be set when the template is instantiated.
A template property can also be declared with the type of a struct instead of a template. Such a property holds a template: any template that derives from the struct can be passed in, and using the property as a component instantiates the template that was passed:
struct StreetLight(on_off: bool)
template MyStreetLight : StreetLight {
prop color: Rgba = {100, 100, 100, 255}
if $on_off {
Emissive: {strength: 1, color: $color}
}
}
template Road {
prop street_light : template StreetLight
lamp {
street_light: {on_off: true}
Position3: {0, 6, 1.5}
}
}
Road my_road(street_light: MyStreetLight)Here my_road.lamp gets a MyStreetLight component with on_off set to true and color at its default, and MyStreetLight's body runs for it. The struct acts as the interface between the template that uses the property and the template that fills it in: the initializer may only set members of the struct, and the value passed must be a template that derives from it - passing an unrelated template, a plain struct or no value at all is an error. In the template's component the property is stored as an entity, so it can be set from C with the template's id. A property of this kind can have a default (prop street_light : template StreetLight = MyStreetLight). When the property is used as a tag (lamp { street_light }), the id it holds is added without instantiating the template body, consistent with using a template as a tag.
The module statement puts all contents of a script in a module. Example:
module components.transform
// Creates components.transform.Position
struct Position(x: f32, y: f32)The components.transform entity will be created with the Module tag.
The module statement must be the first statement of a script.
The include statement loads another script file. Example:
include components
include scenes/level_1.flecsThe path is resolved relative to the directory of the current script. Paths containing .. and absolute paths are not allowed.
If the included path does not end in .flecs, the extension is appended automatically.
When include is used from a managed script (see Managed script), the included script is also loaded as a managed script. If a managed script at that path already exists, it is not loaded again. When used from a non-managed script, the included script is executed in place and no script entity is created.
The include statement is only allowed at the root scope of a script, and cannot appear inside a template. It must appear before any statement other than module and other include statements.
The using keyword imports a namespace into the current namespace. Example:
// Without using
my_engine {
game.engines.FtlEngine: {active: true}
}// With using
using game.engines
my_engine {
FtlEngine: {active: true}
}A using statement must appear at the top of a script, after any module and include statements, and before any other statement. It is not allowed inside scopes or templates. Example:
// OK
using game.engines
my_spaceship {
FtlEngine: {active: true}
}// Not OK: using may not appear inside a scope
my_spaceship {
using game.engines
FtlEngine: {active: true}
}A using statement may end with a wildcard (*). This will import all namespaces matching the path. Example:
using game.*
my_engine {
FtlEngine: {active: true}
}When you're building a scene or asset you may find yourself often repeating the same components for multiple entities. To avoid this, a with statement can be used. For example:
with SpaceShip {
MillenniumFalcon {}
UssEnterprise {}
UssVoyager {}
Rocinante {}
}This is equivalent to doing:
MillenniumFalcon {
SpaceShip
}
UssEnterprise {
SpaceShip
}
UssVoyager {
SpaceShip
}
Rocinante {
SpaceShip
}With statements can contain multiple tags:
with SpaceShip, HasWeapons {
MillenniumFalcon {}
UssEnterprise {}
UssVoyager {}
Rocinante {}
}With statements can contain component values, specified between parentheses:
with Color(38, 25, 13) {
pillar_1 {}
pillar_2 {}
pillar_3 {}
}Scripts can contain variables, which are useful for often repeated values. Variables are created with the const keyword. Example:
const pi = 3.1415926
my_entity {
Rotation: {angle: pi}
}Variables can be combined with expressions:
const pi = 3.1415926
const pi_2 = $pi * 2
my_entity {
Rotation: {angle: pi / 2}
}In the above examples, the type of the variable is inferred. Variables can also be provided with an explicit type:
const wood: Color = {38, 25, 13}When the name of a variable clashes with an entity, it can be disambiguated by prefixing the variable name with a $:
const pi = 3.1415926
const pi_2 = $pi * 2
pi {
Rotation: {angle: $pi / 2}
}Variables can be used in component values as shown in the previous examples. To assign a variable to a component, use the variable as the component expression. The variable name must be prefixed with a $. Example:
const wood: Color = {38, 25, 13}
my_entity {
Color: $wood
}
// is equivalent to
my_entity {
Color: {38, 25, 13}
}Variables can be exported by prefixing a variable declaration with the export keyword. Exported variables can be accessed by the application and from other scripts. The following example shows an exported variable:
// Script 1
export const pi = 3.1415926This variable can now be accessed from another script:
// Script 2
const pi_2 = pi * 2Exported variables are created as children of the scope in which they are defined:
math {
export const pi = 3.1415926
}This will make the variable available to other scripts as math.pi.
The ecs_const_var_init function is used to create exported variables. The following example shows how the same variable can be created from C code:
double pi_value = 3.1415926;
ecs_const_var(world, {
.name = "pi",
.parent = ecs_lookup(world, "math"),
.type = ecs_id(ecs_f64_t),
.value = &pi_value
});Exported variables can be used as configuration that is loaded into an application from a script. The following example shows how to load an exported variable from C after it has been defined in a script or has been created with ecs_const_var_init:
ecs_entity_t pi = ecs_lookup(world, "math.pi");
ecs_value_t v = ecs_const_var_get(world, pi);
double *value = v.ptr;
if (value) {
// Use value
}The following example shows how exported variables can be used in combination with modules in C++:
struct math {
inline static double pi;
math(flecs::world& world) {
world.script()
.filename("math.flecs")
.run();
world.const_var("pi", pi);
}
}
// Import module
world.import<math>();
// Use value
double pi_2 = math::pi * 2;An exported variable declared with const is a compile time constant. Its value
is folded into every expression that uses it, which means that changing the value
afterwards does not affect scripts that already ran.
When a value has to change after a script ran, declare it with mut instead of
const. The value of a mut variable is never folded, and scripts that use it
are reevaluated when it changes:
// Script 1
export mut difficulty: f32 = 1.0// Script 2
enemy {
Health: {100 * difficulty}
}The ecs_mut_var_init function is used to create mutable exported variables from
C code:
float difficulty_value = 1.0;
ecs_entity_t difficulty = ecs_mut_var(world, {
.name = "difficulty",
.type = ecs_id(ecs_f32_t),
.value = &difficulty_value
});To change the value of a mut variable, obtain a pointer to it with
ecs_mut_var_get, and signal the change with ecs_mut_var_modified. Scripts
that use the variable are reevaluated:
ecs_value_t v = ecs_mut_var_get(world, difficulty);
*(float*)v.ptr = 2.0;
ecs_mut_var_modified(world, difficulty);A script is also reevaluated by a change to a mut variable that it declares itself:
export mut difficulty: f32 = 1.0
enemy {
Health: {100 * difficulty}
}An export mut statement never has data dependencies, not even when its
expression uses another mut variable. This means the statement is never
reevaluated by a reactive event, and the variable keeps the value it was
assigned after the script ran.
A script can use the value of a component that is looked up on a specific entity. The following example fetches the width and depth members from the Level component, that is fetched from the Game entity:
grid {
Grid: { Game[Level].width, Game[Level].depth }
}To reduce the number of component lookups in a script, the component value can be stored in a variable:
const level = Game[Level]
tiles {
Grid: { width: $level.width, $level.depth, prefab: Tile }
}The requested component is stored by value, not by reference. Adding or removing components to the entity will not invalidate the component data. If the requested component does not exist on the entity, script execution will fail.
Parts of a script can be conditionally executed with an if statement. Example:
const daytime: bool = false
lantern {
Color: {210, 255, 200}
if $daytime {
Emissive: { value: 0 }
} else {
Emissive: { value: 1 }
}
}If statements can be chained with else if:
const state = 0
traffic_light {
if $state == 0 {
Color: {0, 1, 0}
} else if $state == 1 {
Color: {0.5, 0.5, 0}
} else if $state == 1 {
Color: {1, 0, 0}
}
}Parts of a script can be repeated with a for loop. Example:
for i in 0..10 {
Lantern() {
Position: {x: $i * 5}
}
}The values specified in the range can be an expression:
for i in 0..$count {
// ...
}When creating entities in a for loop, ensure that they are unique or the for loop will overwrite the same entity:
for i in 0..10 {
// overwrites entity "e" 10 times
e: { Position: {x: $i * 5} }
}To avoid this, scripts can either create anonymous entities:
for i in 0..10 {
// creates 10 anonymous entities
_ { Position: {x: $i * 5} }
}Or use a unique string expression for the entity name:
for i in 0..10 {
// creates entities with names e_0, e_1, ... e_9
"e_$i" { Position: {x: $i * 5} }
}Ranges can also be enclosed in brackets:
for i in [0..10] {
// ...
}A range loop can be given a second loop variable, in which case the first variable is the zero-based iteration index and the second variable is the range value:
for (index, value) in [5..10] {
// (0, 5), (1, 6), ... (4, 9)
}For loops can also iterate the elements of arrays, vectors and maps:
for elem in arrayExpr {
_ { Position: {elem, elem * 2} }
}Arrays and vectors can be iterated with an additional index variable, which contains the zero-based index of the current element:
for (index, elem) in arrayExpr {
"e_{index}" { Position: {elem, elem * 2} }
}Maps can be iterated with up to three loop variables. With a single variable the loop iterates the map values. When two variables are specified, the first variable contains the key of the current element. A third variable can be added in the middle, which contains the zero-based iteration index:
for elem in mapExpr {
_ { Position: {elem, elem * 2} }
}
for (key, elem) in mapExpr {
"e_{key}" { Position: {elem, elem * 2} }
}
for (key, index, elem) in mapExpr {
"e_{key}" { Position: {index, elem * 2} }
}Note that the iteration order of maps is undefined.
The continue statement skips the remaining statements of the current iteration and moves the loop to the next iteration:
for i in 0..5 {
if i == 2 {
continue
}
// creates entities e_0, e_1, e_3 and e_4
"e_{i}" {}
}Entities and components that are not created because an iteration was skipped are deleted from the entities of a managed script, just like entities that are no longer created after a script is updated.
A continue statement must appear inside the scope of a for loop. It is not allowed in the root scope of a script, in a template that is declared inside a for loop, or in a function body.
Scripts can define component types by using the type entities from the flecs.meta module (struct, enum, bitmask) as entity kind, followed by an initializer list that describes the type.
A struct is defined by specifying the struct members in the initializer list, where each member is specified as name: type:
struct Position(x: f32, y: f32)The member type can be any registered type, including other types defined in a script. This makes it possible to create nested structs:
struct Point(x: f32, y: f32)
struct Line(start: Point, stop: Point)Members are created as child entities of the struct with the flecs.meta.Member component. The name: type notation is a shorthand that only sets the member type. To specify additional fields of the Member component, assign an initializer to the member instead of a type:
// Member with a type and array size
struct Points(values: {f32, count: 3})The initializer is assigned to the Member component of the member entity, which means all fields of flecs.meta.Member can be set, either by position or by name:
// Same as {f32, count: 3}
struct Points(values: {type: f32, count: 3})
// Member with a unit (requires the units module)
struct Car(speed: {f32, unit: flecs.units.Speed.KiloMetersPerHour})Since members are regular entities, a struct can also be defined by explicitly creating the member entities in the struct scope. The following example is equivalent to struct Position(x: f32, y: f32):
struct Position {
x { member: {type: f32} }
y { member: {type: f32} }
}A struct can inherit the members of another struct by specifying a base struct after the struct name. The derived struct has all members of the base struct, followed by its own members:
struct Point(x: f32, y: f32)
struct Point3D : Point(z: f32)
// Same as
struct Point3D(x: f32, y: f32, z: f32)Inheritance also works with the scope-based syntax:
struct Point3D : Point {
z { member: {type: f32} }
}Under the hood inheritance adds an (IsA, Point) pair to the Point3D entity, which is picked up by the reflection framework. The members of the derived struct are laid out after the base struct, which matches the memory layout of a C struct that embeds the base struct as its first member.
The base struct must be defined before the derived struct, a struct can only have one base struct, and a derived struct cannot redefine a member of its base.
A value of a derived struct can be assigned to a variable, property, mutable or (nested) component member of the base struct type. The assignment copies the members of the base struct:
const p: Point3D = {1, 2, 3}
const q: Point = $p // {1, 2}
my_entity { Point: $p }An enum is defined by listing its constants in the initializer list:
enum Color(Red, Green, Blue)Constants are assigned with incrementing values, starting at zero. In the above example Red has value 0, Green has value 1 and Blue has value 2.
Constants can also be assigned explicitly with the name: value notation:
enum Prio(Low: 1, Medium: 5, High: 10)Implicit and explicit values can be mixed. A constant without a value continues counting from the last assigned value:
// A = 0, B = 10, C = 11
enum Mix(A, B: 10, C)By default enum constants are stored as i32. A different underlying type can be specified by adding a configuration scope to the initializer list with the underlying_type key:
enum Color(Red, Green, Blue, {underlying_type: u64})Constant values must fit in the range of the underlying type.
A bitmask is defined the same way as an enum:
// Bacon = 1, Lettuce = 2, Tomato = 4
bitmask Toppings(Bacon, Lettuce, Tomato)Constants without a value are assigned with incrementing powers of two. Explicit values can be assigned with the name: value notation:
bitmask Flags(A: 1, B: 2, Both: 3)Bitmask constants are stored as u32, which cannot be overridden.
Multiple statements can be combined on a single line when using the semicolon operator. Example:
my_spaceship {
SpaceShip; HasFtl
}This section goes over how to run scripts in an application.
To run a script once, use the ecs_script_run function. Example:
const char *code = "my_spaceship {}";
if (ecs_script_run(world, "my_script_name", code)) {
// error
}Alternatively a script can be ran directly from a file:
if (ecs_script_run_file(world, "my_script.flecs")) {
// error
}If a script fails, the entities created by the script will not be automatically deleted. When a script contains templates, script resources will not get cleaned up until the entities associated with the templates are deleted.
A script can be ran multiple times by using the ecs_script_parse and ecs_script_eval functions. Example:
const char *code = "my_spaceship {}";
ecs_script_t *script = ecs_script_parse(
world, "my_script_name", code);
if (!script) {
// error
}
if (ecs_script_eval(script)) {
// error
}
// Run again
if (ecs_script_eval(script)) {
}
// Free script resources
ecs_script_free(script);If a script fails, the entities created by the script will not be automatically deleted. When a script contains templates, script resources will not get cleaned up until the entities associated with the templates are deleted.
Managed scripts are scripts that are associated with an entity, and can be ran multiple times. Entities created by a managed script are tagged with the script. When script execution fails, the entities associated with the script will be deleted. Additionally, if after executing the script again an entity is no longer created by the script, it will also be deleted.
To run a managed script, do:
const char *code = "my_spaceship {}";
ecs_entity_t s = ecs_script(world, {
.code = code
});
if (!s) {
// the script entity could not be created, for example because the file
// provided in the filename member could not be opened
}The ecs_script function only returns 0 when no script entity could be created at all. When the script code itself fails to parse or evaluate, the script entity is still returned, just like the script entity is kept when ecs_script_update fails. To find out whether the script code was evaluated successfully, test for the EcsScriptError tag, which is added to the script entity when parsing or evaluation failed and removed when the script evaluates successfully:
ecs_entity_t s = ecs_script(world, {
.filename = "my_script.flecs"
});
if (!s || ecs_has_id(world, s, EcsScriptError)) {
// error
}The error message is stored in the error member of the EcsScript component:
const EcsScript *script = ecs_get(world, s, EcsScript);
if (script->error) {
printf("script failed: %s\n", script->error);
}To update the code of a managed script, use the ecs_script_update function:
if (ecs_script_update(world, s, 0, new_code)) {
// error
}Just like ecs_script, a failed ecs_script_update keeps the script entity, adds the EcsScriptError tag to it, stores the error message in EcsScript::error and deletes the entities that were created by the script.
When a script contains templates, script resources will not get cleaned up until the entities associated with the templates are deleted.
Because a managed script keeps track of which statement created which component, a component may only be created for an entity from a single scope, or from scopes that are mutually exclusive. The following script is valid, as the two scopes are branches of the same if statement:
if a {
item { Position: {10, 20} }
} else {
item { Position: {30, 40} }
}The following script is not valid, as both scopes can be evaluated in the same run:
if a {
item { Position: {10, 20} }
}
if b {
item { Position: {30, 40} }
}This only applies to statements that create a component. Declaring the same entity from two scopes is always valid, and so is assigning individual members of a component, which does not create the component:
item {
Position: {1, 2}
}
if a {
item { Position: {x: 10} }
}
if b {
item { Position: {y: 20} }
}By default a script fails when it uses a component, tag, member or function that is not registered in the world. This makes it hard for generic tooling (asset browsers, editors, viewers) to load the scripts of an application without also loading the application code that registers its components.
Lenient loading relaxes this. It can be enabled for a world with ecs_script_set_lenient:
ecs_script_set_lenient(world, true);
if (ecs_script_run_file(world, "my_script.flecs")) {
// error
}It can also be enabled for a single script with the lenient member of ecs_script_eval_desc_t:
ecs_script_eval_desc_t desc = { .lenient = true };
ecs_script_t *script = ecs_script_parse(
world, "my_script_name", code, &desc, NULL);and for a managed script with the lenient member of ecs_script_desc_t:
ecs_entity_t s = ecs_script(world, {
.filename = "my_script.flecs",
.lenient = true
});In lenient mode the following applies:
- A statement that adds an unresolved component or tag to an entity is parsed and discarded. Nothing is created in the world for the unknown name, so a later registration of the real component is unaffected.
- A value assigned to an unresolved component, or to a component without reflection data, is parsed and discarded, including nested
{}and[]blocks. - An unknown member in the value of a known component is parsed and discarded. The members that are known are still assigned.
- An expression that uses an unresolved function, method, identifier or variable is discarded. When the expression belongs to a statement (such as the collection of a
forstatement) the statement is skipped, which means aforover an unresolvable collection iterates zero times. - A
prop,mutorconstdeclaration with an unresolved type is discarded, with or without a default value. The variable is not declared, which means later uses of it inside the template are treated as unresolved identifiers and are skipped like any other unresolved expression. A templatepropthat is dropped is not a member of the template, so assigning it when the template is instantiated is skipped as an unknown member. - References that are structural still cause an error. This includes
IsA(inheritance) references, somy_entity : MyPrefab {}still fails whenMyPrefabcannot be resolved.
Every name that is skipped is reported once per script with ecs_warn, no matter how many times it occurs in the script.
The entities in a script are still created in lenient mode, which means that hierarchies, template instances and the values of components that are registered are loaded as usual:
// Loaded without the game module, with lenient loading enabled
my_lamppost {
Position: {x: 10, y: 20} // registered, assigned
Nightlight: {intensity: 3} // not registered, skipped
HoloCycle // not registered, skipped
}