Skip to main content

16 - No Implicit Use For Function Parameters

From edition=16 onwards, function parameters are no longer implicitly declared with use. This feature was likely to cause accidental bugs.

Previously, every function parameter would be a context variable, as if they had all been declared with use. Some functions, like ImageSprite, take a lot of parameters from context (over 25+), and so it was very easy to coincidentally name a function parameter the same as a context parameter and accidentally pass a parameter into a function that you did not intend to. Not only could this cause unintended bugs, but the implicit nature of this feature made it difficult to find the bugs.

Now all context variables must be explicitly declared with use, making this feature much more intentional and less error-prone. This is a much better design for Easel, and we are changing this now to avoid future problems.

What's changed?

Previously:

  • Every function and subblock parameter would be a context variable, as if they had all been declared with use.

Now:

  • Only function parameters thare are either explicitly declared with use or are the object parameter are context variables.

Function parameters example

This example illustrates the change to function parmaeters:

pub fn unit.Example1(use color, radius) {
// Previously: `unit`, `color` and `radius` would be context variables
// Now: Only `unit` and `color` are context variables
}

Subblock parameters example

This example illustrates the change to subblock parameters:

pub fn SpawnWhirlwind() |use this, power, use durability| { /* ... */ }

pub fn Example2() {
SpawnWhirlwind unit, power {
// Previously: `unit`, `power` and `durability` would be context variables
// Now: Only `unit` and `durability` are context variables
}
}

Motivation

Consider this example:

pub fn unit.Hero(color) {
PolygonSprite(color=)
}

Here we have a function called Hero that draws a PolygonSprite in a particular color.

Now imagine that we want to add a new parameter to Hero, called shadow. Perhaps true means draw a shadow and false means don't draw a shadow.

pub fn unit.Hero(color, shadow) {
PolygonSprite(color=)
if shadow {
PolygonSprite(color=#000, screenOffset=@(0, 1))
}
}

Unfortunately, PolygonSprite takes over 25 parameters from context, and one of them is shadow and so now you are passing a new parameter into PolygonSprite accidentally. You may not even know that PolygonSprite takes a shadow parameter, and so you may be surprised at the unintended effect.

This kind of bug would be difficult to track down because it is implicit - you cannot see that the shadow parameter is being passed into PolygonSprite. It is not visible. You would only know where the bug was if you knew that PolygonSprite takes a shadow parameter.

To stop this kind of bug from happening, Easel no longer implicitly declares function parameters with use. Context is both a powerful but dangerous feature, and changes lets you be intentional about when you want to use context variables, and when you do not.

Alternatives

If you previously had a variable that was relying on being a context variable implicitly, you now have two options.

Option 1: Explicitly declare with use:

pub fn unit.Hero(color, use shadow) {
PolygonSprite(color=)
}
pub fn Example2() {
SpawnWhirlwind unit, use power {
// ...
}
}

Option 2: Pass directly into the function:

pub fn unit.Hero(color, shadow) {
PolygonSprite(color=, shadow=)
}

Recommendations

Context can be both a helpful and dangerous feature. For this reason, we recommend you choose a specific set of variables that you always use a context variables in your project, and stick to that set only. Some common ones could include: body, unit, projectile, radius, color for example. This should be a small set of names that are used repeatedly in many places in your project. Outside of that set, always pass all remaining parameters directly. This way you know what to expect to be in context and are not surprised by their implicit effects as context variables.

See the Context Best Practices for more exposition on this topic.

Upgrade instructions

First, set edition=16 (or higher) in your easel.toml file:

[engine]
edition = 16

Now, when you Preview your game, you will get some errors due to missing arguments. You will need to add an explicit use to make them context variables once again, maintaining their previous behavior. See the Alternatives section for examples.

Unfortunately, because of the nature of this change, there will also be some cases where there will be no error produced, but the behavior is still different. This will be when a function takes optional parameters from context, which can be omitted without error. This is what we recommend:

  1. If you are using ImageSprite in your code for example, look at its list of optional context parameters (the parameters between square brackets [ ] that are declared with a question mark ?), and familiarize yourself with their names. Common ones to look out for: color, opacity, luminous
  2. Take a look at all the main function signatures in your code, and see if you can recognize any of these names in your parameter lists.
  3. Declare them with use to maintain previous behavior.

Acolyte Fight migration instructions

If you are upgrading a project that was remixed from Acolyte Fight, one of the example games of Easel, here are some specific instructions to help you upgrade your project to edition=16.

Acolyte Fight makes heavy use of context variables. Too much so, in fact, if it were being made new today it likely would use this feature a lot less as it introduces a lot of implicit behavior that is difficult to follow sometimes. However, it is what it is, and so we have provided some instructions to help you upgrade your project.

Add use to certain function parameters

The general idea is, whenever a function has one of the following names in its parameter list, we want to add use to it, so that it is a context variable and maintains previous behavior: aoe, color, damage, density, impulse, league, orbit, owner, pastEpisode, radius, selected, shape, turningDecay

You do not need to go through every function signature in the whole codebase. These are the only files which need changing:

  • Every file in the obstacle folder, e.g. boost.easel, ice.easel
  • hero.easel - Hero just needs use radius and use density
  • leaderboards.easel - there are only two changes to make here:
    • LeaderboardSelector needs use selected
    • RenderLeagueMedal needs use league
  • profile.easel - only one change to make:
    • DisplayPastEpisode needs use pastEpisode
  • arrange.easel - ArrangeRing needs use orbit

For example, this is what Boulder looks like before:

pub fn obstacle.Boulder(shape, pos=@(0,0), heading=0, maxHp=50.0, radius=1.0, density=50, turningDecay=0.05, [posOffset?, headingOffset?]) {

And this is what it should look like after, notice the addition of use:

pub fn obstacle.Boulder(use shape, pos=@(0,0), heading=0, maxHp=50.0, use radius=1.0, use density=50, use turningDecay=0.05, [posOffset?, headingOffset?]) {

Bot

In bot.easel, find the group of signals starting with BotChannelling. Change the target parameter to use target, so it looks like this:

pub signal unit.BotChannelling(use target, enemy)
pub signal unit.BotDeflecting(use target, enemy)
pub signal unit.BotAttacking(use target, enemy)
pub signal unit.BotRecovering(use target, enemy)

SpawnContact

Various obstacles use SpawnContact unit, and this needs to be changed to use unit. These will produce errors so you will not be able to miss these.

For example, in ice.easel, this is what it looks like before:

SpawnContact<ice>(filter=Category:Unit) unit {

And this is what it should look like after:

SpawnContact<ice>(filter=Category:Unit) use unit {

If you've made these changes, your project should now be upgraded to edition=16 and you can continue developing your game. Thanks for your continued support of Easel!

Opt-out

If you would like to temporarily opt out your entire project from this change, add useFunctionParamsImplicitly=true to the [legacy] section of your easel.toml, like this:

[legacy]
useFunctionParamsImplicitly = true