Skip to main content

Asynchronous Programming

In Easel, when you want to perform a sequence of steps, one after the other, you should use await so you can list the steps top-to-bottom. This way the shape of the code mirrors the shape of the logic, making it easier to read and understand.

info

The await keyword causes the current thread to pause while waiting for the given event to happen. This allows you to write code in a sequential, top-to-bottom way, even when it is doing things that take time. This type of programming is called asynchronous programming and is a fundamental to how you write code in Easel.

Minimal example

For example, if you have a game that has three levels, and you want to play them one after the other, you should be able to use await to list them one after the other like this:

pub game fn World.Main() {
// ...
await PlayLevel1
await PlayLevel2
await PlayLevel3
await ShowCredits
}

See Awaiting to learn more about how await works in Easel.

Waiting for a condition

Sometimes you need to wait for something to happen before continuing to the next step. You can use a while loop with an await inside to wait for a condition to be true before moving on.

pub await fn InstructionSystem([body, owner]) {
Content<help> { "Use the Arrow Keys to move" }
while Pos.X.Abs < 5 { await Pos }

Content<help> { "Press Spacebar to shoot" }
await ButtonDown(Spacebar)
}

Short-lived entities

Let's say you're making a sailing game. Sometimes, sharks will appear and attack the player's ship. While the sharks are present, the sea becomes rough and dangerous to sail on.

If your storm is a system called StormSystem, then you can use a short-lived entity to own that system, just for that step, and then expire the entity when the sharks are gone:

pub await fn this.PlayLevel1() {
Content { "Another day on the Pacific Ocean!" }

await Tick(10s)

SharkAttack(numSharks=10)
await Spawn {
TopContent { "Watch out!" }
StormSystem

while QueryAny(filter=Category:Shark) { await Tick }
Expire
}
}

When the shark attack is over, it expires the entity, automatically removing the TopContent and StormSystem from the game.

Any time you have components, systems, behaviors which you want to run while waiting for the condition to be met, you can use this pattern.

Marking functions as blocking

When a function is going to wait for something before returning to the caller, it should always be marked as await fn. This will make sure your function behaves in a predictable way and can be easily composed with other code.

For example, this is function will block the game for at least 1 minute, which is why we have correctly declared it as await fn:

pub await fn PlayLevel1() {
LaunchMarauders(10)
await Tick(30s)
LaunchCorsairs(5)
await Tick(30s)
LaunchBoss(1)
while QueryAny(filter=Category:Alien) { await Tick }
}

How to avoid blocking the caller

What if we want to use await but we don't want to block the caller? We like how await allows us to write our code in a sequential, top-to-bottom way, but we want to make sure our caller can compose our function with other code without worrying about it blocking.

One way you can deal with this is to Spawn an entity to run the sequential blocking code on another thread, allow the function to return immediately.

pub fn PlayLevel1() {
Spawn {
LaunchMarauders(10)
await Tick(30s)
LaunchCorsairs(5)
await Tick(30s)
LaunchBoss(1)
while QueryAny(filter=Category:Alien) { await Tick }
}
}

Another way is to add a behavior to a given this entity:

pub fn this.PlayLevel1() {
behavior {
LaunchMarauders(10)
await Tick(30s)
LaunchCorsairs(5)
await Tick(30s)
LaunchBoss(1)
while QueryAny(filter=Category:Alien) { await Tick }
}
}

This has the advantage that when this expires, the behavior will automatically stop, even if it is not complete. Depending on your use case, this may be desirable or undesirable, so choose the pattern that best suits your needs.

If you choose to use a behavior, in Easel, this is what we would call a system.