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 subblock parameters
Add two pipe characters || to the function signature to declare a delve parameter
which takes no subblock parameters.
fn FunctionWithSubblock() || {
delve() // call the subblock
}
fn Example() {
FunctionWithSubblock {
// When `delve(...)` is called above, it goes into this subblock section here
}
}
With subblock parameters
In the function signature, you can specify a comma-separated list of subblock parameters inside the pipe characters (|).
When calling the function, separate each parameter with a comma as well.
The example below shows a function that has a subblock that takes two subblock parameters, x and y.
fn FunctionWithSubblock() |x, y| {
delve(123, 456) // call the subblock with x=123, y=456
}
fn Example() {
FunctionWithSubblock x, y {
// receives x=123, y=456
}
}
Implicit context subblock parameters
If a subblock 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.
In the example below, the posOffset parameter is declared as an implicit context variable in the subblock,
even though there is no mention of it 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 subblock, posOffset is an implicit context variable,
// even though it is never mentioned in the subblock.
PlaceObstacle(
pos=@(2,3),
size = index < 3 ? 3 : 10, // make first 3 smaller
// the posOffset parameter is passed in implicitly from context
)
}
}
this subblock parameter
If a subblock parameter is prefixed with use this = ,
then it always becomes a variable named this in the subblock,
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.
fn FunctionWithSubblock() |use name, use age| {
delve(name="Augustus", age=25)
delve(name="Petunia", age=26)
delve(name="Russell", age=27)
}
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 subblock parameters
When the subblock receives parameters, they are matched by name first, then by position. This means they may be received in a different order than they are declared in the function signature.
pub fn Spray() |index, headingOffset| {
// ...
}
pub fn Example() {
Spray headingOffset, index {
// Notice the parameters `headingOffset` and `index` are received in a different order than declared.
// This still works because in this form, parameters are matched by name where possible.
}
Spray i {
// In this case, `i` will be bound to the declared parameter `index`
// because there is no parameter named `i`
}
}
Explicit binding
Use newName=oldName to explicitly choose the parameter to bind to a value when calling the subblock.
This can be used to give the parameters new names, or to bind them in a different order than they are declared.
pub fn Shimmer() |color, radius| { /* ... */ }
pub fn Example() {
Shimmer r=radius, c=color {
// ...
}
}
Ignoring parameters
It is possible to skip the next positional parameter by using _ as a placeholder.
pub fn Shimmer() |color, radius| { /* ... */ }
pub fn Example() {
Shimmer _, r {
// The first parameter is ignored, and the second parameter `radius` is bound to `r`
}
}
Declaring into context with use
Declare any parameter as a context variable in the subblock by prefixing it with use:
pub fn Shimmer() |color, radius| { /* ... */ }
pub fn Example() {
Shimmer color, use radius {
// The parameter `radius` is declared as a context variable in this subblock because of `use`
}
}
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!") }
}
pub fn Example1() {
TellMeHowManyToMake {
return 5 // This value is returned from the subblock and assigned to `numToMake`
}
}
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)
}
}
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" }
}
}