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:
This section goes over the basic syntax over Flecs Script.
An entity is created by specifying an identifier followed by a scope. Example:
An entity scope can contain components and child entities. The following example shows how to add a child entity:
Note how a scope is also added to the child entity.
To create anonymous entities, leave out the entity name:
Alternatively, the _ placeholder can be used to indicate an anomyous entity:
The _ placeholder can be useful in combination with syntax constructs that require an identifier token, such as inheritance:
Entity names can be specified using a string. This allows for entities with names that contain special characters, like spaces:
String names can be combined with string interpolation (see below) to create names that are computed when the script is evaluated:
By default children are created using the ChildOf hierarchy storage. To select the Parent hierarchy storage, add the tree annotation:
A tag can be added to an entity by simply specifying the tag's identifier in an entity scope. Example:
Pairs are added to entities by adding them to an entity scope, just like tags:
Components are specified like tags, but with an additional value:
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:
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:
Components can be defined in a script (see Type definitions):
Components can be pairs:
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 (.):
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:
Multiple singleton components can be specified in the same scope:
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:
This is equivalent to doing:
When using the entity kind syntax, the scope is optional:
If the specified kind is a component, a value can be specified between parentheses:
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:
Newlines function as statement separators. Multiple statements can be combined on a single line with the semicolon (;) operator:
Newlines after opening a scope ({) or before closing a scope (}) are not mandatory:
Applications can specify the following builtin kinds which provide convenience shortcuts to commonly used features:
Scripts can natively specify inheritance relationships between entities, which is useful in particular for prefabs. Example:
The : notation is short for adding an IsA relationship with the relationship syntax:
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:
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:
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:
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:
while this is an invalid usage of an initializer:
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:
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):
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:
Initializer expressions may contain add assignment (+=) or multiply assignment (*=) operators. These operators allow an initializer to modify an existing value. An example:
This can be especially useful when used in combination with templates (see below):
Match expressions can be used to conditionally assign a value. An example:
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:
Match expressions can be used to assign components:
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 ;:
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.
An expression 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:
To reduce the number of component lookups in a script, the component value can be stored in a variable:
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:
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:
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:
All features that are supported by entity statements are also available for new expressions, such as the ability to have children:
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:
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:
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:
The following example shows how to use an expression:
To prevent evaluating expressions in an interpolated string, the $ and { characters can be escaped:
Interpolated f32 and f64 values can include a format specifier after the expression, separated by a colon:
The complete syntax is:
| 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:
Width and precision can be integer literals, variables, or parenthesized expressions.
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:
For equality expressions (using the == or != operators), additional rules are used:
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:
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:
For example:
An example of a vector operation:
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:
Lvalues are the left side of assignments. There are two kinds of assignments possible in Flecs script:
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:
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:
Another notable example where this matters is for enum and bitmask constants. Consider the following example:
Here, 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:
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:
C
C++
Define the callback before registering the function:
C
C++
The following syntax can be used to define a function in a script:
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:
Control flow statement such as if and for are not allowed inside of a function. To expression conditional logic, functions can use match expressions:
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:
Methods are defined outside of scripts by using the Flecs Script API.
A method can be created in code by doing:
C
C++
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:
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:
Vector functions are registered like normal functions, but instead of specifying a callback, the application sets vector_callbacks. An example:
C
C++
The signature for vector functions accepts an additional argument for the number of elements in the vector type. Define these callbacks before registering the function. The f32 implementation is shown below; use double for the vector elements in lerp_f64:
C
C++
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:
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:
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:
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:
Scripts can contain variables, which are useful for often repeated values. Variables are created with the const keyword. Example:
Variables can be combined with expressions:
In the above examples, the type of the variable is inferred. Variables can also be provided with an explicit type:
When the name of a variable clashes with an entity, it can be disambiguated by prefixing the variable name with a $:
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. Example:
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:
This variable can now be accessed from another script:
Exported variables are created as children of the scope in which they are defined:
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 native code:
C
C++
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 native code after it has been defined in a script or has been created with ecs_const_var_init:
C
C++
The following example loads math.flecs from a native module. The file defines export const pi = 3.1415926 in the module scope:
C
C++
An export const variable may not be modified over its lifetime. To create a variable that is allowed to be changed, use export mut:
The ecs_mut_var_init function is used to create mutable exported variables from native code:
C
C++
Updating a mut variable notifies reactive scripts that depend on it:
C
C++
Parts of a script can be conditionally executed with an if statement. Example:
If statements can be chained with else if:
Parts of a script can be repeated with a for loop. Example:
The values specified in the range can be an expression:
When creating entities in a for loop, ensure that they are unique or the for loop will overwrite the same entity:
To avoid this, scripts can either create anonymous entities:
Or use a unique string expression for the entity name:
Ranges can also be enclosed in brackets:
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 loops can also iterate the elements of arrays, vectors and maps:
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:
A continue statement skips the remaining statements of the current iteration and moves the loop to the next iteration:
The header of a script may contain module, include and using statements. After the first non-header statement, no more header statements may occur. Header statements must always be created in the root scope.
The module statement will create a module entity with the specified name, and create all script contents in that module. A script may only contain a single module statement. Example:
The game module will be created with the flecs.core.Module tag.
Module statements may be specified as paths:
The include statement loads another script file. Example:
The 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.
If a script contains a module statement, include statements must appear after the module statement.
The using keyword imports a namespace into the current namespace. Example:
If a script contains module or include statements, using statements must be placed after both.
A using statement may end with a wildcard (*). This will import all namespaces matching the path. Example:
Managed scripts are reactive, which means they will be reevaluated when the data that they depend on changes. A managed script is one that uses the following API:
See "Managed scripts" for more details. The following sections go over the reactivity features of flecs script.
The following example illustrates a simple reactive script:
The value of the Emissive component depends on whether game[TimeOfDay].daylight is smaller than 0.5. This condition is not just evaluated when the script runs. It will be treated as an invariant, meaning that if game[TimeOfDay].daylight changes, the value of Emissive must change as well.
The script runtime implements "fine grained reactivity". In short this means that flecs tracks which parts of a script depend on which inputs, and that when an input changes, only the code that depends on that input is ran. For this example that means it will only set Emissive and not Position.
Value dependencies are tracked recursively. An input can be assigned, modified and stored through many indirections. The script runtime will still pick it up as dependency. For example, in the following code example Emmissive will still change when TimeOfDay changes:
Reactive updates are non-destructive. This means that entity handles remain valid before and after the update. For this example it means that light will still be the same entity before and after TimeOfDay changes. This is useful, as it means that we can safely add components to entities defined in scripts.
Scripts responds to the following reactive inputs:
Scripts additionally respond to entities or components not yet existing by deferring their execution. For example, when a script refers to game[TimeOfDay] but the game entity doesn't exist yet, or it doesn't have TimeOfDay yet, the script will monitor the world for those entities to become available.
Scripts subscribe to OnSet events to get notified of component changes. This works out of the box with operations such as set() or assign(), but if a component reference returned by ensure() or get_mut() is assigned, it needs a separate call to modified() for the script to see it. Additionally, if a system modifies a component directly, it will also have to call modified() on the component:
Scripts can have entities whose existence depends on a reactive value. For example:
This script has a different kind of invariant: the light entity must only exist when daylight is lower than 0.5. When TimeOfDay is modified with a daylight value higher or equal to 0.5, the script will delete the light entity.
Scripts can have components whose existence depends on a reactive value. For example:
The invariant in this script is that the light entity must only have the Emissive component when the value of daylight is below 0.5. If the value is higher or equal to 0.5, the script will remove the Emissive component from the light entity.
A challenge with deciding whether a component has to be removed is that it could be defined in more than one scope, for example:
To satisfy this script, when the value of daylight becomes lower than 0.5, Emissive should not be removed. Its value should instead be changed to {0}. This turns a local decision ("branch not taken, remove component") into a global decision ("was there any other scope that assigned `Emissive`"). Things get more complex with multiple conditional assignments:
To handle these cases correctly, a script would have to build a table of all conditions affecting a component, and assign an action based on which conditions are true. To avoid this complexity, scripts enforce a simple rule that sidesteps this problem: components must be owned by a single scope. This means that the above two examples will throw an error.
A component may be assigned in two mutually exclusive scopes:
Scopes are currently only considered mutually exclusive when they are in the if, else or else if branches of a single if chain. The following example will not parse, even though strictly speaking the scopes are mutually exclusive:
Additionally, components may be partially assigned in multiple scopes, as long as there is one scope that owns the component:
Entities created in a loop do not survive a reactive update, unless they are named. For example, here is an example of a loop that creates count anonymous entities:
When count changes, all previous anonymous entities will be deleted before the for loop is reevaluated. To prevent this from happening, entities in a loop must be named:
This will create a named slot for each entity that is tracked across reactive updates, which prevents deleting the entities on each update.
While somewhat wasteful, this following example also work as expected:
This will create a single entity named e with Position: {count, count * 2}. When count is 0, e will be deleted. Similarly, this also works:
This also creates a single entity named e with Position: {count, count * 2}. When count is 0, Position will be removed.
Consider the following script:
Now imagine that TimeOfDay is assigned many times, from 0.1 to 0.11, 0.12, 0.13, 0.14, ... We would be doing a lot of redundant work, essentially assigning the Emissive component to the same value on each update, unless the value changes to above 0.5, at which point Emissive changes value.
To avoid this kind of overhead, isDay is treated as a computed value. Computed values are cached between updates, so that subsequent updates can tell whether code actually needs to be reran. This happens automatically to significantly reduce the amount of redundant work that reactive updates do.
Templates are reactive parameterized scripts that can be used to create procedural assets. Templates can be created with the template keyword. A simple example:
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:
Templates are commonly used in combination with the kind syntax:
Templates can be parameterized with prop variables. To create a prop variable, use the prop keyword. Example:
Prop variables are reactive, just like how a component value (game[TimeOfDay]) is reactive. This means that when the value of a prop changes, the template is reevaluated, following the same rules as described above.
Templates can have mut variables. A mut variable is reactive state that is not exposed as a prop. For example, a button may have a hover mut variable. Changes to the mut variable will cause the template to update (just like with props), but unlike props, hover is not passed to the template.
Code outside of a template can access mut variables:
Templates can use the this variable to refer to the current instance. For example, the following code:
will cause Bob to end up with (Likes, Bob).
The this variable can be used to read other components of the template instance:
Templates can inherit from each other. This can be used to create templates that accept and instantiate other templates. For example, consider we want to create a Building template with a customizable facade. We could build a template like this, but we would have no way to instantiate the facade because we do not know its type:
Instead, what we can do is define a Facade base type and have a template inherit from it:
We can then use the Facade type in the prop definition, and instantiate the template-specific facade:
This makes it possible to use templates as primitive for procedural generation templates, where a generic template specifies the "grammar" of an object (for example a building), with a set of derived templates that implement the style and/or content.
A template can require its immediate parent to be an instance of another template:
The parent template must be defined before the constrained template. Instantiating Facade without a parent that has Building produces an error.
A constrained template can read the parent's props and muts as ordinary variables. Local variables, props, and muts mask parent members with the same name. Use parent.height to explicitly access a parent member. Reads are reactive: changing the parent's props or muts updates dependent children.
Parent constraints compose with inheritance:
Derived templates inherit the base template's parent constraint, so : Facade also suffices here. An explicit constraint must agree with the inherited constraint.
Children can assign parent muts in async blocks. Parent props remain read-only. This lets a group own shared state while its children handle input:
Use parent.active = label if a local variable masks active. Changes to parent muts update reactive output without restarting the child's async blocks; subsequent reads in those blocks use the current parent state.
To update template props from native code, mirror the template type with a native type that has the same name, namespace and members. An example:
C
C++
Setting mut variables works in a similar way, but with a type called mut that is in the scope of the template:
C
C++
The C++ example allocates the string because setting an rvalue transfers its ownership to the reflected component.
The following code shows a more complex example with templates that create children and uses nested templates:
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:
This is equivalent to doing:
With statements can contain multiple tags:
With statements can contain component values, specified between parentheses:
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:
The member type can be any registered type, including other types defined in a script. This makes it possible to create nested structs:
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:
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:
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:
In this example Point3D values will have x, y and z members. The Point3D type will have an (IsA, Point) relationship.
An enum is defined by listing its constants in the initializer list:
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:
Implicit and explicit values can be mixed. A constant without a value continues counting from the last assigned value:
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:
Constant values must fit in the range of the underlying type.
A bitmask is defined the same way as an enum:
Constants without a value are assigned with incrementing powers of two. Explicit values can be assigned with the name: value notation:
Bitmask constants are stored as u32, which cannot be overridden.
This section goes over how to run scripts in an application.
To run a script once, use ecs_script_run in C or world.script_run in C++:
Alternatively a script can be run directly from a file:
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 run multiple times by parsing it once and evaluating it repeatedly. In C++, the returned flecs::parsed_script owns the parsed script and frees it when it goes out of scope. It can be moved, and must be destroyed before its world.
C
C++
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 discoverable and modifiable in the world. A managed script is associated with an entity. To create a managed script, do:
C
C++
To load a managed script from a file, set .filename = "game.flecs" in C or use .filename("game.flecs") in C++ instead of setting the code. A missing file can prevent the script entity from being created.
To update the code of a managed script, use ecs_script_update in C or world.script_update in C++:
C
C++
When a managed script contains code that has errors, the managed script will still exist in the world. To discover whether a managed script has errors, use the following code:
C
C++