Context Parameters
Easel has an unusual feature called context. In certain situations when a function call is missing 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.
Games are often made through a process of iteration and experimentation. Used to its greatest effect, context can greatly reduce the amount of boilerplate code you need to write, making your code smaller, lighter and easier to experiment with.
Context is resolved at compile time and so is equally efficient as passing in parameters manually. Missing context parameters will be detected at compile-time and reported as errors. One of the concerns people may have about implicit context is unintended consequences, but because Easel's context parameters are verified at compile time, you can be more confident that your code will work as expected.
Context can be a powerful but dangerous tool. See Context Best Practices for recommendations on how to use context safely and effectively.
Minimal Example
When a function is missing context parameters, the Easel compiler will look for context variables that have the same name as the missing parameters, and use them to fill in the missing parameters. Not every variable and not every parameter is eligible for this treatment, but we will discuss the exceptions and the rules later.
The most straightforward way to declare a context variable is with a use statement:
fn this.Example() {
use color=#ff8800 // declare a context variable
Spark(radius=5) // What color is the spark? It is #ff8800 because of context
}
Context parameters
Not every function parameter may be filled from context. Parameters that can be filled from context are called context parameters. These are declared in two ways:
- Parameters declared inside square brackets (
[and]), - Or the function's object parameter, may be filled from context.
Example of the square bracket notation:
// `color` and `radius` may be filled from context, but `name` may not be
fn Hero(name, [color, radius]) { /* ... */ }
fn Example() {
use color=#ff8800
Hero("Augustus", radius=5)
// Equivalent to: Hero("Augustus", radius=5, color=#ff8800)
}
The object parameter is also always found from context too.
// `ship` is the object parameter, and therefore is also a context parameter
fn ship.ActivateWarpDrive { /* ... */ }
fn Example() {
use ship = Spawn
ActivateWarpDrive
// Equivalent to: ship.ActivateWarpDrive
}
Context variables
When Easel is looking for a value for a context parameter, only context variables are considered. Not every variable is a context variable. This subsection lists all the ways to declare a context variable.
Variables declared with use
The most common way to declare a context variable is with a use statement.
fn Example() {
use color=#ff8800, radius=5 // declare two context variables
Hero("Augustus")
// Equivalent to: Hero("Augustus", color=#ff8800, radius=5)
}
fn Hero(name, [color, radius]) { /* ... */ }
Function parameters inside [ and ]
In the function parameter list, any parameters declared between [ and ] are not just context parameters,
they are also context variables by default.
You can override this by declaring them with let.
// `color` is a context variable, but `radius` is not
fn Example([color, let radius]) {
Hero("Augustus") // Equivalent to: Hero("Augustus", color=#ff8800)
}
fn Hero(name, [color, radius?]) { /* ... */ }
Object parameter
The object parameter of a function is always implicitly a context variable.
In addition, it is also declared as a context variable named this.
// `ship` is the object parameter and so therefore also a context variable
// Also there is an implicit `use this=ship`
fn ship.RedirectPowerToShields() {
GiveMeAShield // Equivalent to: this.GiveMeAShield
SendDistressCall // Equivalent to: SendDistressCall(ship=)
}
fn this.GiveMeAShield() { /* ... */ }
fn SendDistressCall([ship?]) { /* ... */ }
In the above example, if a function call needs either ship or this from context, they will be given ship,
the object parameter of the function. Both names are valid and can be used interchangeably for ship.
By convention, functions will add their components to this.
See this to learn more.
Subblock parameters declared with use
Subblock parameters can be declared as context variables too, using the use keyword:
pub fn Example() {
MultiShot angleOffset, use index {
// `index` is a context variable here, but `angleOffset` is not
}
}
pub fn MultiShot() |angleOffset, index| { /* ... */ }
Implicit context variables from subblocks
If a subblock parameter is declared with use keyword in its function declaration,
then it is automatically declared as a context variable in the subblock.
This feature is only used in one case in Easel - the magic ui parameter.
Since ui is used everywhere, this reduces boilerplate code dramatically.
If you are using it for anything else, be careful, as it can make your code harder to read and understand.
// The `use ui` makes `ui` an implicit context variable in the subblock of DigitCard
fn DigitCard(heading, [ui]) |use ui| { /* ... */ }
pub fn player.Example() {
Content {
DigitCard("Games Played") { %(player.NumLifetimeGames) }
DigitCard("Outlasts") { %(player.NumLifetimeOutlasts) }
DigitCard("Wins") { %(player.NumLifetimeWins) }
}
}
In the above example, use ui declares ui as an implicit context variable.
Compare this to the example below, where ui is passed explicitly.
Notice that the code is much more verbose. This is why ui is often declared as an implicit context variable.
pub fn player.Example() {
Content ui {
DigitCard("Games Played", ui=) ui { ui.%(player.NumLifetimeGames) }
DigitCard("Outlasts", ui=) ui { ui.%(player.NumLifetimeOutlasts) }
DigitCard("Wins", ui=) ui { ui.%(player.NumLifetimeWins) }
}
}
See UI to learn more about the ui context variable.
Implicit this from subblocks
Prefix a subblock parameter with use this = to always declare it as this in the subblock.
// `use this` makes `this` an implicit context variable in the subblock of SpawnGrab
pub fn SpawnWhirlwind() |use this = whirlwind| {
Spawn whirlwind {
delve(whirlwind)
}
}
pub fn Example() {
SpawnWhirlwind whirlwind {
// `this` is an implicit context variable in this block
ImageSprite(@whirlwind.svg, radius=10)
// Equivalent to: this.ImageSprite(@whirlwind.svg, radius=10)
}
}
See this to learn more about the very important this context variable.
Implicit use for use this
If a subblock parameter is declared with use this,
then whatever name is used for the this variable in the subblock is also a context variable.
pub fn SpawnWhirlwind() |use this = whirlwind, power| {
Spawn this {
delve(this, power=9000)
}
}
pub fn Example() {
SpawnWhirlwind foo, power {
// `foo` is a context variable, but `power` is not
}
SpawnWhirlwind bar, power {
// `bar` is a context variable, but `power` is not
}
}
Another example of this rule is the built-in Spawn function,
which you will undoubtedly use everywhere in your game.
Whatever name you give to your entity becomes a context variable because Spawn is declared with use this.
pub fn Example() {
Spawn unit {
// `unit` is a context variable
}
Spawn projectile {
// `projectile` is a context variable
}
}
Non-context variables
Variables declared with let or const are not context variables.
fn Example() {
let color = #ff8800 // not a context variable
use radius = 5 // context variable
Hero("Augustus")
// Equivalent to: Hero("Augustus", radius=5)
}
fn Hero(name, [color?, radius?]) { /* ... */ }
Enclosing scopes only
Only variables in the current scope or in enclosing scopes are considered candidates when searching for a parameter from context.
fn Hero([name?, age?]) { }
fn Example() {
use name="Augustus", age=25
Spawn {
Hero
// Equivalent to: Hero(name="Augustus", age=25)
// Both `name` and `aged` passed in from context,
// even though they are in an enclosing scope, not the current scope
}
}
When a function calls another function, it is not an enclosing scope and so context variables do not leak from one function into another.
fn Hero([name?, age?]) { }
fn Example() {
use name = "Augustus" // add `name` to context
InnerFunction
}
fn InnerFunction {
// `name` is not in context here,
// even if we are called from `Example`
use age=25
Hero // Equivalent to: Hero(age=25)
}
That means, in general, the only variables that can be passed in from context are the ones you can see. This is important as it avoids unexpected bugs. It is not possible for functions far away in the call stack to cause unanticipated side effects.
Best practices
Context can be a powerful but dangerous tool, so
choose a limited set of context variable names that you use everywhere in your project,
for example unit, projectile, body, owner, color and radius.
See Thinking Context for a deeper discussion of this topic.
Recap
Context is a powerful feature that allows your Easel code to be more concise and expressive. Context has been specifically designed so that it is as useful as possible while still being safe and predictable.
Context parameters will automatically find their value from context variables, unless explicitly given a value.
Not every parameter is a context parameter. Context parameters are:
- Declared inside square brackets
[and] - Or the object parameter of a function (e.g.
fn ship.ActivateWarpDrive,shipis a context parameter)
Not every variable is a context variable. Context variables are:
- Declared with
use:use color=#ff8800 - Are object parameters:
fn unit.Hero() { }-unitis a context variable - Or in the case of
thisorui, are implicitly declared within the subblocks of certain functions. It is possible for you to declare other variables to work like this as well, but this should be done sparingly.
Any let or const variables are not context variables and so can be used freely without worrying about
unexpectedly affecting the context.
Context is a powerful feature that can greatly reduce the amount of boilerplate code you need to write, enabling you to experiment more easily and create better games, faster.