Skip to main content

Behaviors

A behavior is a concurrently executing block of code that is attached to an entity. A behavior might be responsible for a player's movement, a bullet's trajectory, or a countdown timer, for example. A single entity often has many behaviors. A behavior automatically gets attached to the current entity (this) when it is created. When an entity despawns, all of its behaviors are automatically stopped.

info

In some programming languages, the term fiber is used to describe a lightweight thread of execution that runs concurrently with the rest of the program. Easel has fibers too, but we call them behaviors because they are part of entities in the game world. This allows them to be managed in an integrated way with everything else in the game.

There are four types of behavior blocks. Simple behavior blocks concurrently execute a block of code straight away. In addition to this, on, once and with blocks define blocks of code that are concurrently executed when certain events occur.

behavior

A behavior block executes concurrently with the rest of the program. This allows for multiple streams of logic to run at the same time.

pub fn World.Example() {
behavior { // Starts a concurrent block of code
loop {
Transmission { "Jump!" }
await Tick(5s)
}
}
Transmission { "Simon says:" }
}
// Outputs "Simon says:" once, and "Jump" every 5 seconds

once

A once block creates a concurrent behavior that first awaits an expression, then executes its block of code once. If the this entity despawns before the awaited expression returns, the block of code will never be executed.

pub fn World.Example() {
once Tick(1s) {
Transmission { "world!" }
}
Transmission { "Hello" }
}
// Outputs "Hello", then "world!" after 1 second

on

An on block runs each time an event occurs. Specifically, on creates a concurrent behavior that awaits its expression, then executes its block of code, then loops back to the start of the block where it repeats the process. This repeats forever until its this entity despawns or a break statement is encountered.

pub fn World.Example() {
on Tick(1s) {
Transmission { "and again" }
}
Transmission { "Hello again" }
}
// Outputs "Hello again", then "and again" every second

with

A with block runs once unconditionally, then each time an event occurs. Specifically, with creates a concurrent behavior that executes its block of code once unconditionally (without evaluating its expression), then loops back to the start where it awaits its expression before executing its block of code again. This repeats forever until its this entity despawns or a break statement is encountered. with blocks are used to setup an initial state and then update the state each time it changes.

pub prop this.Health
pub fn World.Example() {
Health = 100
with Health {
Transmission { "Health: " + Health }
}

Health = 50
}
// Outputs "Health: 100" then "Health: 50"

What can be awaited?

on, once and with blocks call await on their expression, and so can only be used with functions declared with an await fn specifier. If called on a function that does not have an await specifier, the compiler will error. See await to learn more about what can be awaited.

pub await fn WaitForOneSecond() { // must be an `await fn`
await Tick(1s)
}
pub fn World.Example() {
on WaitForOneSecond {
// ...
}
}

Using return values from await expressions

once and on blocks can receive return values from the expression they await by declaring a space-separated list of parameters before the body.

// must be an `await fn` to be used with `on`
pub await fn WaitForMessage() -> string {
await Tick(1s)
return "Hello, world!"
}
pub fn World.Example() {
// `message` takes its value from the return value of `WaitForMessage`
on WaitForMessage message {
Transmission { %(message) }
}
}
// Outputs "Hello, world!" every second

The values are destructured the same way as subblock parameters and so may be declared in a different order to the return values of the expression. That is, first they are destructured by name if possible, and then by position.

// must be an `await fn` to be used with `on`
pub await fn WaitForPerson() -> name, age {
await Tick(1s)
return "Augustus", 25
}
pub fn World.Example() {
// `age` and `name` are destructured by name so can be out-of-order
on WaitForPerson age name {
Transmission { "Hello " + name + ", you are " + age + " years old!" }
}
}
// Outputs "Hello Augustus, you are 25 years old!" every second

withs (not once or on) specifically do not receive return values from the expression they await. This is because they skip execution of their await expression the first time they run and so do not have return values the first time.

Awaiting multiple expressions

A once, on or with block can await multiple expressions, separated by commas. The block will execute when the first expression returns. The other expressions will be terminated. The block will receive the return value from the first expression only if the expression returns a single value, otherwise it will receive undefined.

pub await fn WaitForACarrot() -> vegetable {
await Tick(1s)
return "Carrot"
}
pub await fn WaitForAPotato() -> vegetable {
await Tick(2s)
return "Potato"
}
pub fn World.Example() {
on WaitForACarrot, WaitForAPotato vegetable {
Transmission { %(vegetable) }
}
}
// Outputs "Carrot" every 1 seconds

Lifespan

Every behavior is owned by the current entity (this). When this despawns, all of its behaviors are terminated.

pub field vegetable.NumMessages
pub fn Example() {
Spawn vegetable { // `this` is the vegetable entity
NumMessages = 0
on Tick(1s) {
Transmission { "Potato" }
NumMessages += 1

if NumMessages >= 3 {
Despawn // despawn the vegetable
}
}
}
}
// Outputs "Potato", "Potato", "Potato" then stops

Behavior replacement

Only one behavior with a given ID may exist on an entity at a time. When a behavior is created on a given entity, it will replace a previous behavior that was previously created on that entity with the same ID. If an ID is not specified for a behavior, a unique ID will be automatically generated. These rules work exactly the same as with other components with IDs, but they will be restated here specifically for behaviors for clarity.

A behavior may be given an explicit ID by specifying it in angle brackets after the behavior keyword. If you are using an once, on or with block, insert the behavior keyword before the once, on or with keyword.

pub fn Example() {
behavior<hello> on Tick(1s) {
Transmission { "Hello" }
}
behavior<hello> on Tick(1s) { // replaces previous behavior<hello>
Transmission { "HELLO" }
}
behavior<world> on Tick(1s) { // different ID, so creates a new behavior
Transmission { "world!" }
}
}
// Outputs "HELLO" and "World" every one second

If a behavior has no explicit ID, then an ID will be automatically generated for it. Normally, entities need many behaviors, and this makes it easy to add as many behaviors as you need to an entity without needing to manually set their ID.

When using automatic IDs, a unique ID is assigned to each line of code. This can cause one situation which may be surprising. That is, calling the behavior block in a loop will replace the previous instance.

pub fn Example() {
// This loop does not create 5 separate behaviors.
// Instead it keeps replacing the same behavior with a new one.
// Only the last created behavior will be active.
for i in RangeInclusive(0, 5) {
// This is like: behavior<auto123> on Tick(1s)
// The behavior gets replaced each time the loop repeats
// because it always has the same ID
on Tick(1s) {
Transmission { %("The number is " + i) }
}
}
}
// Outputs "The number is 5" every one second

Deleting behaviors

Behaviors can be deleted using the delete keyword, followed by an ID to identify the behavior.

pub fn Example() {
Spawn potato {
behavior<sayWhoIAm> on Tick(1s) {
Transmission { "I am a Potato" }
}

await Tick(5s)

delete behavior<sayWhoIAm>
}
}

Massive Concurrency

A game often consists of many entities, each with many behaviors, and Easel is designed to handle this efficiently. When a behavior encounters an await, it goes to sleep and does not take up any processing power. Even if there are thousands of concurrent behaviors running at once, often only a small percentage of them wake up each tick, making Easel very efficient.

Easel's concurrent programming model allows Easel games to be written in a more natural way, with each independent stream of logic able to be laid out linearly step-by-step.