Lifecycle and Identity
Evolve's template system works on a concept called Structural Identity
. This means that the compiler
figures out which 'scope' an element belongs to and ties that element's lifecycle to the scope. A scope is
basically just a sequential list of statements inside a render
block.
There are two types of context
in Evolve, a uicontext
applies to statements inside a render
block and the
code
context applies everywhere else, including expressions. The code
contexts works identically to C# and as
such there is no Evolve magic happening under the hood, where the uicontext
applies lots of magic and allows the
use of state
, enable
, create
, and other Evolve statement constructs.
Scopes are created whenever we use a new piece of control flow, so if
, match
, repeat
, slot
, render
all
create new scopes. All elements declared in that scope are bound to its lifecycle, so if the scope is disabled or
destroyed, so are the elements inside of it.
This means we only create elements when we enter a scope for the first time, and we disable or destroy elements
when the scope is no longer in use. Every frame as your templates execute the system figures out if the control flow
being run was also run last frame or not and decides if it needs to create the elements within this scope or if
it should use the same instances from last frame. One of the great things about this system is that you can declare
state
variables anywhere inside a uicontext
scope, and they will retain their values across frames.
Disabling & destruction happen async after the frame finishes, so if you have handlers registered for those they will fire at the start of the NEXT frame, before anything else happens.
Here is how a frame is built for each template in your game.
- Was this the first time this scope was entered?
- if so then invoke any
created
hooks that are defined on the elements
- if so then invoke any
- Was this created this frame or previously disabled?
- if so then invoke any
enabled
hooks
- if so then invoke any
- Set all the per-frame 'bindings' for the currently executing element. This includes:
- parameters
- non constant styles
- non constant attributes
- invoke any before early input hooks
- process input
- invoke any after early input hooks
- invoke any update hooks
- invoke any after update input hooks
- recursively visit all the children of this template
- invoke any late input hooks
- invoke any finish hooks
template Example(required bool showGroup1) render {
if(showGroup1) {
// a new scope is created for the true case
// the two text elements below are bound to this scope
// if this branch does not execute in a given frame,
// all elements that are descendents of this scope will be
// disabled (or destroyed if the scope is marked with :destructive)
Text("Element 1");
Text("Element 2");
Action xyz = () => {
// everything in here is C#, so there are not scopes and you cannot use
// Evolve constructs like `state` or `enable` or `repeat`
};
// A 'run' statements lets you run arbitrary C# code at this exact point in time
run {
// this is also a C# context
}
// both sides of this if statement create a new scope. Which ever branch of this
// if statement is NOT entered in a given frame, those elements will be disabled
// (or destroyed if the :destructive specifier is used)
if (someOtherValue) {
Text("More elements here");
}
else {
Text("More elements here");
}
}
else {
// a new scope is created for the else case
// Element 3 is a member of this scope.
Text("Element 3");
}
}
Asynchronous Lifecycle
There are two classes of events which are not executed as your code runs each frame: disable
and destroy
.
For performance reasons we only run these handlers at the start of the next frame. It is illegal / undefined behavior if you have a disable/destroy handler that uses a closure to close over any fields / state in a template. Generally, you should not have to worry about this because the system makes it very hard to do, but in case you are tempted to trick the system into doing something crazy: you've been warned!