Skip to main content

IDs

IDs let you refer back to components that were created previously, in order to update or delete them for example. Easel has a unique feature called Implicit IDs, where the ID is automatically assigned from the line number. Combined with a with block, this lets you code in a reactive style where components get replaced when state changes, without you needing to know about IDs at all.

info

Sometimes the world Discriminator is used to more explicitly refer to IDs as ID is a fairly generic word that can mean many things.

Specifying IDs in function calls

IDs are specified using the FunctionName<Id> syntax. In code, IDs are often written as Id to match Pascal case conventions.

pub fn this.Example() {
// Two different ImageSprites with different IDs
ImageSprite<a>(image=@spiral.svg, radius=5)
ImageSprite<b>(image=@spiral.svg, radius=10)

await Tick(5s)

// Replace the ImageSprite with the ID <b>
ImageSprite<b>(image=@spiral.svg, radius=15)
}

Declaring IDs in the function signature

When you declare a function, you can specify that it has an ID by using the <Id> syntax:

pub fn this.Tornado<Id>(...) {
ImageSprite(...)
}

Implicit IDs

IDs may be declared as implicit using the auto keyword. This means that the ID is automatically assigned by the compiler. You will most commonly encounter this when using sprite functions like ImageSprite, which has a function signature similar to this:

pub fn this.ImageSprite<Id=auto>(...)

The <Id=auto> means that instead of calling ImageSprite like this:

ImageSprite<alienShip>(@alienShip.svg, color=#66ff00)

You can just write this:

ImageSprite(@alienShip.svg, color=#66ff00)

The ID will be automatically assigned. Internally, if this call is happening in a file called file1.easel on line number 5, it looks something like this:

ImageSprite<file1.easel/line5>(@alienShip.svg, color=#66ff00)

Updating components using a with block

Games often need to update components in response to changes in state.

For example, we want our ship's color to change from green to red as it loses health. We can do this by combining Automatic IDs and a with block, like this:

pub const MaxHealth = 100
pub prop this.Health = MaxHealth
pub fn this.Example() {
with Health {
ImageSprite(@alienShip.svg, color=color=(Health / MaxHealth).BlendHue(#ff6600, #66ff00))
}
}

The with block runs its code once up front, and then once again each time its property value changes. That means that ImageSprite gets called multiple times in the example above. However, because ImageSprite always has the same implicit ID (e.g. file1.easel/line5), instead of creating a new sprite each time, the previous ImageSprite always gets replaced with a new one. This is a key pattern that you will use everywhere in your Easel code.

To make it explicit, it is like writing this:

pub const MaxHealth = 100
pub prop this.Health = MaxHealth
pub fn this.Example() {
with Health {
ImageSprite<file1.easel/line5>(@alienShip.svg, color=color=(Health / MaxHealth).BlendHue(#ff6600, #66ff00))
}
}

The ID for ImageSprite is always the same (file1.easel/line5), which is why it gets replaced each time Health changes.

info

This style of programming is called Reactive Programming, most famously used by the React library for web development. This is why we sometimes refer to Easel as a reactive game engine.

Forwarding IDs

IDs can be forwarded to another function. This is useful when a function with an ID wants to be able to identify its own components, without getting mixed up with the components created by other invocations of the same function.

pub fn this.DoSomethingCool<Id>(radius) {
// Make sure our ImageSprite is ours by giving it our ID
ImageSprite<Id>(image=@spiral.svg, radius=)

await Tick(5s)

delete ImageSprite<Id>
}

// The function below is actually sharing the same ID for all calls to ThisWouldNotWork,
// meaning different calls with different IDs interfere with each other
// which is probably not what you want.
pub fn this.ThisWouldNotWork<Id>(radius) {
// Different calls to ThisWouldNotWork use the same ImageSprite ID,
// so they interfere
ImageSprite<abc>(image=@spiral.svg, radius=)

await Tick(5s)

delete ImageSprite<abc>
}

The plus (+) sign can be used to concatenate symbols and create a new sub-ID. This is useful when one function with an ID needs to manage its own multiple subcomponents, without getting mixed up with the components created by other invocations of the same function.

pub fn this.DoSomethingCool<Id>(radius) {
ImageSprite<Id+a>(image=@spiral.svg, radius=)
ImageSprite<Id+b>(image=@dodecahedron.svg, radius=)

await Tick(5s)

delete ImageSprite<Id+a>
delete ImageSprite<Id+b>
}

Implicit IDs and forwarding

Implicit IDs are automatically concatenated with the parent ID. That means that any components you create will by default be kept separate from other components created by different invocations of the same function, without you needing to worry about it!

pub fn this.Spiral<Id>(radius) {
// Similar to writing ImageSprite<Id+auto123>(...)
// where `auto123` is an example of an implicit ID
ImageSprite(image="spiral.svg", radius=)
}
pub fn this.Example() {
// The ImageSprites created by these two Spiral calls
// do not interfere with each other
Spiral<a>(radius=5) // Creates an ImageSprite<a+auto123>
Spiral<b>(radius=10) // Creates an ImageSprite<b+auto123>
}

File-level and global IDs

If an ID is specified beginning with a lowercase letter, then it is a declared at the file level. This allows other functions in the same file can refer to the same ID. Functions in other files cannot use the same ID and would instead be using their own independent file-level ID.

// file1.easel
pub fn this.SameFileExample1() {
ImageSprite<small>(image=@spiral.svg, radius=5)
ImageSprite<large>(image=@spiral.svg, radius=10)
}
pub fn this.SameFileExample2() {
// This is in the same file, so this would replace the ImageSprite<large> created by SameFileExample1
ImageSprite<large>(image=@spiral.svg, radius=15)
}

// file2.easel
pub fn this.DifferentFileExample() {
// This is in a different file, so this would create a new independent ImageSprite<large>
ImageSprite<large>(image=@spiral.svg, radius=15)
}

If an ID is specified beginning with an uppercase letter, then the compiler will find the symbol in the global scope, and use it as a global ID.

// spiral.easel
pub symbol Fred // declare a globally-unique symbol named Fred

// file1.easel
pub fn this.File1Example() {
ImageSprite<Fred>(image=@spiral.svg, radius=10)
}

// file2.easel
pub fn this.File2Example() {
// Even though this is in a different file, 'Fred' refers to the same symbol,
// and so this would replace the Spiral<Fred> created by File1Example
ImageSprite<Fred>(image=@spiral.svg, radius=15)
}

Optional IDs

An ID may be marked as optional by using the ? suffix. Note, this doesn't mean that the function doesn't have an ID, it just means that it defaults to null, which means all instances of the function will share the same ID unless specified otherwise. This is suitable for functions like PolygonCollider where it makes sense to normally only have one instance of the component.

// '?' suffix indicates that Id is optional
pub fn this.Spiral<Id?>(radius) {
ImageSprite<Id>(image="spiral.svg", radius=)
}

pub fn this.Example() {
Spiral(radius=5)
Spiral(radius=7) // this replaces the Spiral from the previous line because they both have no ID

Spiral<large>(radius=10) // separate ID, so does not replace previous Spiral
}