Skip to main content

Key Concepts

Easel is a unique programming language with some unusual features.

This section covers a few key concepts that, once you understand, will make everything in Easel make a lot more sense. This section will only give a broad introduction to these concepts. More detail can be found throughout the rest of this documentation.

Entities

Entities are the backbone of an Easel program. Entities are spawned using functions like Spawn or Subspawn. The World is a special global entity that can be used to own things that should last for the duration of the program.

Spawn greeting {
Content { "Hello, I am a new entity!" }
}

Components

Almost everything in Easel is a component that gets attached to an entity. This includes:

pub prop hero.Health

Spawn hero {
Body(pos=@(3,7), velocity=@(1,2)) // body component

ImageSprite(image=@hero.svg, radius=7) // sprite component

Health = 100 // prop component

on Tick { // behavior component
Spark(color=#0f0)
}
}

Replacement

Games are about putting players into ever-changing worlds that respond to their actions. Creating dynamic, interactive worlds is both the challenge and the joy of game development.

In Easel, dynamics are created by replacing components over time. For example, sprites can be replaced every tick to create animation.

Normally, when you create a component, it will replace any previous components of the same type with the same ID. The ID is specified in angle brackets after the function call.

// Displays "Hello" for 1 second before replacing it with "World"
Content<greeting> { "Hello" }
await Tick(1s)
Content<greeting> { "World" }

Don't worry, IDs are normally automatically generated so you don't normally need to worry about them!

// No explicit IDs for these ImageSprites, which means the compiler automatically generates them
ImageSprite(image=@hero.svg, radius=7) // equivalent to ImageSprite<auto1>
ImageSprite(image=@crown.svg, radius=5) // equivalent to ImageSprite<auto2>

One of the great things about Easel though is that the ID is unique to the line of code, so replacing a component can be as simple as executing the same block of code in a loop. Using a with block, it is easy to create an entity that responds to changes to its state.

// Each time Health changes, replace the ImageSprite with a new one of different color
with Health { // this is a loop that runs once immediately, then again each time Health changes
let p = Health / MaxHealth
ImageSprite(@hero.svg, color=p.BlendHue(#ff6600, #66ff00)) // equivalent to ImageSprite<auto123>
// The ImageSprite will be replaced on each iteration of the loop
// because the ImageSprite has the same ID every time
}

Despawning

When an entity despawns, all of its components are removed.

The Despawn function despawns an entity immediately, whereas Expire causes an entity to despawn at the end of the tick, during its reap phase.

// Displays "Hello, world!" for 5 seconds, then removes the message
Spawn greeting {
Content { "Hello, world!" }
await Tick(5s)
Despawn
}

Even though behaviors contain executing code, they are just components like any other and so will stop executing when the entity despawns.

// Displays "Hello, world!" 5 times, then stops
Spawn greeting {
on Tick(5s) { // behavior component - this stops when `greeting` despawns
Transmission { "Hello, world!" }
}

await Tick(5s)
Despawn
}
info

Easel is a concurrent programming language. The trouble with concurrent programming is when you spin up all your independent routines of logic, you need to make sure to stop them all at the right moment. Easel never has free routines running around, they are components of entities, and so do not run a moment longer than they should. This eliminates a whole class of bugs and makes concurrent programming accessible to beginners.

Fields and Props

Games are made of many entities, each with their own unique state. The most common way to store data on entities is with fields and props.

pub field shield.PreviousShield
pub prop unit.Health = 100
pub prop ability.Cooldown = 0

What makes a prop different from a field is that you can use keywords like await or on to respond to changes in a prop. This is useful when creating interesting, dynamic worlds that react to change.

pub prop owner.NumCarrots = 0

// Displays "You now have 7 carrots!" then after 5 seconds displays "You now have 23 carrots!"
pub fn Example1(owner) {
on NumCarrots {
Transmission { "You now have " + NumCarrots + " carrots!" }
}

NumCarrots = 7
await Tick(5s)
NumCarrots = 23
}

Public identifiers

A single entity in Easel often ends up with many props from many different subsystems of code. For example, a single hero entity might have a Health, Cooldown, and Score props. Easel helps you ensure that two parts of your code don't accidentally overwrite each other's data by requiring you to declare your props up front.

Normally when you declare a prop, it is private and can only be accessed from the file it is declared in. However, if you declare a prop using the keyword pub, it is public, meaning it is accessible from all files.

prop hero.Cooldown // Private - accessible only within this file
pub prop hero.Health // Public - accessible from any file

If you try to declare two public properties with the same name, Easel raise a compile-time error. There is no problem with declaring two private properties with the same name. Easel will store them separately.

All of this is designed so that you can be sure no parts of your code are interfering with each other unexpectedly.

Awaiting

Often in games, it is useful to wait for something to occur before performing some action. This could be when a prop changes, when a signal is sent, or when a certain amount of time has passed.

Blocking await

A function can await for something to happen. This will make the current thread go to sleep until the condition is met.

// Displays "Hello, world!" after 5 seconds
await Tick(5s)
Transmission { "Hello, world!" }

Waiting concurrently

Alternatively, behaviors can be used to split off from the calling function and perform the waiting concurrently. This spawns a new lightweight thread (a fiber), allowing the main thread to continue executing. Easel is a highly concurrent language, and so often games are made up of thousands of concurrently-executing behaviors.

A once block will run once and then stop.

// Displays "Hello" immediately, then "World!" after 5 seconds
once Tick(5s) {
Transmission { "World!" }
}
Transmission { "Hello" }

An on block will run repeatedly each time the condition is met.

// Displays "Hello, world!" every 5 seconds
on Tick(5s) {
Transmission { "Hello, world!" }
}

A with block runs once immediately and then repeatedly each time the condition is met. This is useful in situations like the case below, where we should display our hero sprite immediately. If we waited until the hero's health changed before displaying the sprite, our hero would be invisible to start with, which would be wrong.

with Health {
let proportion = Health / MaxHealth
ImageSprite(@hero.svg, color=proportion.Mix(#f00, #0f0))
}

The current entity this

One of the most important concepts in Easel is the current entity.

In general, when you create a component in Easel (for example, an ImageSprite), it becomes attached to the current entity, whatever that may be. When an entity despawns, all of its components will be removed as well.

Which entity is the current entity? Normally, the current entity is the subject parameter of the current function's signature. For example, if the function signature is ship.Spaceship, the current entity is ship. However, there is one exception to this rule. Whenever you call a function that spawns a new entity like Spawn, the current entity will instead be the newly-spawned entity within the subblock.

pub fn ship.Spaceship() {
// the current entity is `ship` inside this block
ImageSprite(@sparkles.svg, radius=5) // Attaches a sprite to the current entity, `ship`

Spawn projectile {
// `projectile` is now the current entity inside this block
ImageSprite // Attaches a sprite to the current entity, `projectile`
}
}

In code, you can use the name this to refer to the current entity.

How would you know that a function is going to use the current entity? Click here to take a look at the documentation for ImageSprite. Notice that it is defined as this.ImageSprite. That is how you can tell that ImageSprite is going to be attached to the current entity, this.

How would you know if a function is going to change which entity is the current entity? Click here to take a look at the documentation for Spawn. Notice the part that says |use this = entity| { } That tells you that the current entity will be changed to the newly-spawned entity within the subblock.

See the documentation on this to learn more about this.

Context

Easel has an unusual feature called Context. When a function is called, it will automatically pull in the parameters it needs from context. If a function call is missing certain parameters, the Easel compiler looks for variables with the same names as the function parameters in the surrounding code, and automatically wires them up for you. There are particular rules around this to stop this causing unexpected side-effects.

pub fn ship.Spaceship() {
use color=#8800ff // The `use` syntax puts `color` into the context

// Both the function calls below require a `color` but it has not been given explicitly.
// Easel will automatically pull in the `color` parameter from context.
PolygonSprite(Circle(radius=5))
Spark(radius=1)
}

Which variables can be used as implicit parameters?

The use keyword declares variables that can be used as implicit parameters when calling other functions. That is the primary purpose of the use keyword.

Variables declared with let or const are never passed into another function automatically. They are truly local to the function they are declared in.

In addition to variables declared with use, all parameters listed in the function signature can also be used as implicit parameters.

pub fn ship.PlasmaBolt(color, [owner]) { // ship, color and owner are all implicit parameters
use radius=10 // implicit parameter
let x = 5 // not an implicit parameter

// Equivalent to `ship.DoSomething(color=color, owner=owner, radius=10)`
// `x` is not passed from context because it is declared with `let`
DoSomething
}

pub fn ship.DoSomething([color?, owner?, radius?, x?]) { /* ... */ }

Which parameters will be filled from context?

Not every function parameter will be filled from context. Only the subject parameter (the parameter before the . in the function signature) and parameters declared inside square brackets [ ] will be filled from context.

pub fn Example1() {
SpawnEachPlayer owner {
Subspawn ship {
use color = #bb00ff

// Equivalent to `ship.Spaceship(owner=owner)`.
// `color` is not pulled in from context
// (see the signature for `Spaceship` below to see why)
Spaceship
}
}
}

// `ship` and `owner` will be found from context,
// but `color` is not because it is not inside square brackets [ ]
pub fn ship.Spaceship(color=#ff8800, [owner]) { /* ... */ }

There are a few more rules beyond this, but this is enough to get you started. See Context for more detail.

info

Context is a powerful feature that allows Easel to help you wire up your code. It is designed not just to reduce the boilerplate, but to make it easier to tinker and experiment with your game.

Subblocks

Another unusual feature in Easel are Subblocks. This gives Easel its hierarchical style, which allows game logic to be expressed with less indirection than other programming languages, making it easier for beginners to understand and manipulate.

Let's say we have this generic function that allows us to spray out a certain number of projectiles in an arc over a given duration:

pub await fn Spray(arc, count) |headingOffset| { /* ... */ }

The |headingOffset| is the unique part that you would not see in another programming language. This declares that the function Spray takes a subblock of code, and that subblock will be given a parameter called headingOffset each time it is called. The Spray function would call this subblock as many times as it likes, whether that be zero, one or more times. It depends on the purpose of the function.

When calling the Spray function, the subblock is provided after the function call like this:

pub fn hero.CastFiresplatter(speed=10) {
await Spray(arc=0.3rev, count=10) headingOffset {
// This is the subblock for `Spray`
Spawn projectile {
Body(
pos = hero.Pos,
velocity = speed * Direction(hero.Heading + headingOffset),
)
// ...
}
}
}

The Spray function could be defined like this:

pub await fn Spray(arc, count) |headingOffset| {
for i in Range(0, count) {
delve(headingOffset = arc * ((i / count) - 0.5)) // call the subblock
await Tick
}
}

As you can see, delve is used to call the subblock at the appropriate time.

tip

If you are familiar with other programming languages, you can think of a subblock parameter as a callback parameter. They are built into the Easel language so they can be expressed more naturally and concisely, while also allowing the Easel compiler to verify they are used correctly. This helps make this powerful feature more accessible to beginners.

Conclusion

Easel has a few novel features that are designed to make it a great tool for learning to code and making games. These are its more prominent unique features, but there are many more to discover throughout the rest of this documentation. A great place to get started is the Quickstart tutorial.