Skip to main content

Subblocks

Easel has a unique feature called Subblocks which allows a function to be called with a subblock of code as a parameter. This gives Easel its unique hierarchical style, which allows game logic to be expressed in a more natural way with less indirection than other programming languages.

The subblock is passed into the function as a callback, assigned to a special variable named delve. The function can then call the delve variable at the appropriate time to execute the callback.

Function signature

Callback parameters

Add two pipe characters || to the function signature to declare a delve parameter which takes no callback parameters.

fn FunctionWithSubblock() || {
delve() // call the subblock
}
fn Example() {
FunctionWithSubblock {
// When `delve(...)` is called above, it goes into this callback section here
}
}

In the function signature, you can specify a comma-separated list of callback parameters inside the pipe characters (|). When calling the function, separate each parameter with a space.

fn FunctionWithSubblock() |x, y| { // comma-separated when declaring
delve(123, 456) // call the subblock with x=123, y=456
}
fn Example() {
FunctionWithSubblock x y { // space-separated when calling
// receives x=123, y=456
}
}

Context callback parameters

If a callback parameter is prefixed by the use keyword, then it will become a context variables in the subblock, even if they are not declared in the subblock.

fn ArrangeRing(numSteps, orbit) |index, use posOffset| {
for i in Range(0,numSteps) {
delve(
index=i,
posOffset=orbit * Direction(1rev * i / numSteps),
)
}
}
fn PlaceObstacle(pos, size, [posOffset]) {
SpawnObstacleAt(pos + posOffset)
}
fn Example() {
ArrangeRing(numSteps=10, orbit=5) index {
// Inside this block, posOffset is an undeclared context variable.

PlaceObstacle(
pos=@(2,3),
size = index < 3 ? 3 : 10, // make first 3 smaller
// the posOffset parameter is passed in implicitly from context
)
}
}

this callback parameter

If a callback parameter is prefixed with use this = , then it always becomes a variable named this in the callback, in addition to any other name it may be declared with.

fn this.DrawMe() { }
fn SpawnProjectile() |use this = projectile| {
Spawn projectile {
delve(projectile)
}
}
fn Example() {
SpawnProjectile fireball {
DrawMe // the object parameter `this` is assigned from context
}
}

Calling the subblock

The subblock may be called zero or more times within the function by calling the delve parameter like any other function. If delve is null or undefined, then calling it will have no effect, but will also not error. If delve is not called with the correct number of parameters, an error will be generated at compile time.

Any parameters for the delve call declared with use will be filled from context if they are not explicitly provided.

fn FunctionWithSubblock() |use name, use age| {
use name="Augustus", age=25
delve() // name and age are passed in from context
}

Receiving callback parameters

When calling a function, the arguments to the subblock may be declared in a different order than they appear in the function signature. Arguments are first matched by name where possible, then by position. It is possible to skip the next positional parameter by using _ as a placeholder. It is also possible to declare a parameter with a different name using newName=oldName syntax.

pub fn Spray() |index, layer, color, radius| {
// ...
}

pub fn Example() {
Spray i _ radius c=color {
// The first callback argument 'i' is bound to the parameter 'index'
// because there is no parameter named 'i' and so the compiler falls back to matching by position.
// The parameter 'layer' is ignored because it is skipped using '_'.
// The 'radius' parameter is found by name, and so it does not matter that its position is incorrect here.
// The 'color' parameter is renamed to 'c' and found by name.
}
}

Returning values from a subblock

You can use any values that are returned from the subblock.

fn owner.TellMeHowManyToMake() || {
let numToMake = delve()
Transmission { %("Time to make " + numToMake + " widgets!") }
}

Awaiting the subblock

Calling a subblock waits for it to fully complete. This includes waiting for any awaits that the subblock does. Unlike other programming languages, you do not need to use the await keyword to wait when calling the subblock, just use delve() like normal.

The example below displays "Waiting for 5 seconds...", then "Done!".

fn FunctionWithSubblock() || {
delve() // Waits until the subblock is complete before returning
Transmission { "Done!" }
}

pub game fn World.Main() {
FunctionWithSubblock {
Transmission { "Waiting for 5 seconds..." }
await Tick(5s)
}
}
info

This works because Easel is a stackful coroutine language, which means it can suspend and resume the execution of any function at any point. In other programming languages you need to specify resume points using the await keyword, but not in Easel.

Optional subblocks

You can make a subblock optional using ? after the subblock parameter list.

pub fn this.FunctionWithSubblock() |abc|? {
delve(123)
}

pub game fn World.Main() {
FunctionWithSubblock // subblock is optional
FunctionWithSubblock {
Transmission { "Hello from the subblock" }
}
}

You can just call delve like normal, and if the subblock is not provided, the call will do nothing. If you do really want to check whether a subblock has been provided or not, you can check if it is undefined or not.

pub fn this.FunctionWithSubblock() |abc|? {
if delve {
delve(123)
} else {
Transmission { "No subblock provided" }
}
}