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 await
s 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" }
}
}