Skip to main content

Event-Driven Programming

Easel is an event-driven programming language, which means that normally your code waits for events, like a button press, a collision, or a timer, and then runs in response to those events.

tip

Some game engines use a frame-by-frame model where an entity will update every frame. While you can do this in Easel, it is not the most efficient way to program your game. Make your entities wait for events instead of updating every frame. Let your entities sleep!

In general, you respond to events using on, once or with blocks. You can also await for certain events to happen, putting the current thread to sleep until the event occurs. To learn about the basics of using these language features, see Behaviors or Await.

This page covers common solutions to common problems when coding in an event-driven manner using Easel.

Responding every tick, but with a condition

One more tricky case you may come across is when you want to respond every tick, but only under certain conditions.

  • In a spaceship game, applying thrust to your ship every tick, but only when the player is holding down the Click button.
  • In a parkour game, applying upward force to your character every tick, but only when the player is holding the jump button.
  • In an RPG game, applying a healing effect to your character every tick, but only when the player is in a healing zone.

In this section we will explore some patterns you can use to implement this behavior. To do this, we will focus on the spaceship example, but the same patterns can be applied to any of the other examples.

Naive solution

The simplest solution is to check the condition every frame.

on BeforePhysics {
if IsButtonDown(Click) {
Velocity += Heading.Direction * 0.5
}
}

This works, but it executes every frame, even when the player is not pressing Click. If there is only one ship in the game, this is not a big deal. If there are hundreds, consider one of the following solutions instead.

Else clause await form

You can make a minor adjustment to the previous example by adding an await in the else clause. Since our condition is if IsButtonDown(Click), we can await IsButtonDown(Click) and it will put the current thread to sleep until the player presses Click again.

on BeforePhysics {
if IsButtonDown(Click) {
Velocity += Heading.Direction * 0.5
} else {
await IsButtonDown(Click)
}
}

This is a minimal code change from the previous example and achieves our goal of letting the thread sleep when the player is not pressing Click.

The problem with this one is, if your if statement has many lines of code in it, then the IsButtonDown(Click) and the await IsButtonDown(Click) may be separated by many lines of code, maybe more than one screen, and so it is difficult to see they are related.

Early continue form

This form keeps the IsButtonDown(Click) and the await IsButtonDown(Click) together:

on BeforePhysics {
if !IsButtonDown(Click) {
await IsButtonDown(Click)
continue
}

Velocity += Heading.Direction * 0.5
}

The IsButtonDown(Click) and the await IsButtonDown(Click) are now together at the top of the block, making it easy to see their relationship. Additionally, this form reduces cognitive load because the condition is dealt with and exited early, reducing the nesting level for the rest of the block.

This is quite good, but it still has to execute once to discover the IsButtonDown(Click) is false before the thread goes to sleep.

Behavior add/remove form

This form only creates the behavior when the player is pressing Click, and removes it when they are not:

on ButtonDown(Click) {
behavior<thrust> on BeforePhysics {
Velocity += Heading.Direction * 0.5
}
}
on ButtonUp(Click) {
delete behavior<thrust>
}

When the button is clicked, the behavior is added and will execute every tick (during the BeforePhysics phase, see Anatomy of a Tick). When the button is released, the behavior is removed and will no longer execute.

This is a good solution, but it requires you to name the behavior (behavior<thrust> in this case). Some beginners also have trouble understanding the abstract nature of the behavior being added and removed because it is a little indirect.

This is our recommended pattern for responding to events continuously:

on ButtonDown(Click) {
while IsButtonDown(Click) {
Velocity += Heading.Direction * 0.5
await BeforePhysics
}
}

We recommend this pattern because:

  • It is the most succinct of all forms
  • It executes linearly from top-to-bottom, with no branching or jumps, making it the most straightforward to read
  • No naming required of the behavior (e.g. behavior<thrust> in the previous example). Sometimes this indirection can be hard to follow for people new to Easel.
  • It is trivial to change the waiting interval to something longer, e.g. await BeforePhysics(0.5s) instead of await BeforePhysics. In some of the other forms above, naive changing BeforePhysics to on BeforePhysics(0.5s) would introduce a delay to the first iteration by 0.5s because their delay happens at the top of the loop, whereas this form has its wait at the bottom. This makes it the most flexible of all forms.

The one thing to be wary of is, if you forget the await BeforePhysics line at the bottom, you will have created an infinite loop. Easel's built-in loop limit will eventually stop the loop, but you may have to wait a few seconds.

All things considered, this is the recommended pattern for responding to events every tick, but with a condition.