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 function that uses subblocks that you will become most familiar with is Spawn. When you call Spawn, the subblock is used to define the main behavior of the entity you are spawning.

Spawn {
// This is the subblock for `Spawn`
// It is called when the entity is spawned
// and is used to define the main behavior of the entity
}

Here is an example of a 3 levels subblocks, used to welcome players into a multiplayer game:

pub game fn World.Main(maxHumanPlayers=5) {
SpawnEachPlayer owner {
Spawn hero {
Content(audience=Audience:All) {
"Welcome to the Hotel California, " + PlayerName + "!"
}
}
}
}

In other game engines, you would need to define each of these 3 levels as separate functions, maybe even in separate files. This would make it hard to see the whole picture of how the game works. Easel's subblocks let the hierarchical relationships of your code structure match the hierarchical relationships within your game. It can be much easier to read and understand.

Declaring functions with subblocks

No 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
}
}

With callback parameters

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 be declared as a context variable in the subblock, even if not explicitly 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. Conventionally, you would do this when you have just spawned a new entity, and want the subblock to setup the entity.

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
}
}

See This for more information on the special this variable.

Calling the subblock

The subblock may be called zero or more times within the function by calling the special delve parameter. 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.

use parameters found from context

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.

Checking if a subblock has been provided

delve will be undefined if not subblock has been provided:

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