Shields
Open up your project from the Quickstart tutorial. We are going to keep adding to it.
Let's make the game more interesting by adding a powerup which you can pickup to gain a shield that protects you for one hit. This will demonstrate how to create a temporary effect in Easel.
In Easel, you create temporary effects by creating a temporary entity. When an entity despawns, all of its components are removed. In this section we are going to create a shield entity, attach some things to it, and then you will see how its effects stop when the shield entity despawns.
Creating a shield
We will need to create two categories, one for the powerup and one for the shield.
Select your categories.easel
file. Insert the following highlighted lines underneath the other categories:
pub tangible category Category:Ship
pub tangible category Category:Asteroid
pub tangible category Category:Projectile
pub tangible category Category:Shield
pub tangible category Category:Powerup
Now we need to define a function for the shield. The shield is simply going to be a collider that we attach to the ship. We will make the shield bigger than the ship so that everything will collide with the shield first. Dangerous asteroids will unable to reach the ship itself.
Create a new file called shield.easel
. Fill it with the following lines of code:
pub fn shield.Shield(ship) {
use body=ship, owner=ship.Owner
use radius=2, luminous=1
PolygonCollider(shape=Circle, category=Category:Shield, density=0)
PolygonSprite(shape=Circle, opacity=0.5, ownerColor=true, shine=0.1, bloom=3, shading=0.1, shadow=0.5, layer=-5)
}
We now need to define a function for the powerup. When the powerup collides with a player's ship, it will spawn a shield on their ship, and then despawn itself.
In your shield.easel
file, insert the following highlighted lines at the bottom of your file,
after the Shield
function you just inserted:
pub fn shield.Shield(ship) {
// ...
}
pub fn powerup.ShieldPowerup() {
use body=powerup
use radius=0.9, speed=10, luminous=1
Body(
pos = (BoundaryRadius + radius + 5) * RandomVector,
velocity = speed*RandomVector,
)
PolygonSprite(shape=Circle, color=#00ffaa, opacity=0.5, bloom=3, shadow=0.5)
ImageSprite(@shield.svg, radius=0.85*radius)
PolygonCollider(shape=Circle, category=Category:Powerup, collideWith=Category:None, sense=Category:Ship, density=1)
RecoverSpeed
DecayTurnRate
WrapOutsideBoundary
on BeforeCollide that {
if that.Category.Overlaps(Category:Ship) {
use ship = that
ship.Subspawn shield {
Shield(ship=)
}
Expire
break
}
}
}
Next we need to insert the image file used for the shield powerup. Insert the following file into your project. Right click then Save link as..., then drag-drop it into the file list of your project.
Now we need to make the powerup spawn in the game.
Select your main.easel
file.
Insert the following two snippets of code into your Main
function:
pub game fn World.Main() {
// ...
once AfterGameCommenced {
behavior<spawnAsteroids> with Tick(2s) {
const MaxAsteroids = 25
if QueryCount(filter=Category:Asteroid) < MaxAsteroids {
Spawn asteroid {
Asteroid
}
}
}
behavior<spawnPowerups> with Tick(5s) {
if !QueryAny(filter=Category:Powerup) {
Spawn powerup {
ShieldPowerup
}
}
}
}
once AfterGameConcluded {
delete behavior<spawnAsteroids>
delete behavior<spawnPowerups>
}
// ...
}
Click Preview. You should soon see a shield powerup appear. Go pick it up! You will see that you are now you are impervious to asteroids.
The current entity this
Let's take a moment to understand one of the most important concepts in Easel - the current entity.
To do this, let's take a closer look at the code we just wrote for ShieldPowerup
.
Open up shield.easel
and find your ShieldPowerup
function so you can follow along.
Below you will find a snippet of some of the relevant parts of the ShieldPowerup
function for quick reference.
pub fn powerup.ShieldPowerup() {
// ...
PolygonSprite(shape=Circle, color=#00ffaa, opacity=0.5, bloom=3, shadow=0.5)
ImageSprite(@shield.svg, radius=0.85*radius)
// ...
on BeforeCollide that {
// ...
}
}
In the ShieldPowerup
function we create various components like
an ImageSprite and a PolygonSprite.
But these components do not last forever. When we collect the powerup, all of its sprites disappear!
How does Easel know when to remove these components?
In Easel, components follow one simple rule.
Whenever you create a component in Easel, by default it becomes part of the current entity.
In this case, the current entity is our powerup
.
That is why when the powerup
despawns, all of its components despawn with it.
Why is the current entity the powerup
?
Because of the function signature fn powerup.ShieldPowerup()
.
The parameter before the .
defines what the current entity will be for the function.
Whenever you create a component in Easel, by default it becomes part of the current entity.
The current entity can normally be found by looking for the parameter before the .
in the function signature.
For example fn powerup.ShieldPowerup()
means the current entity is powerup
.
How many components does the ShieldPowerup
function add to the powerup
?
Go to shield.easel
and try counting them yourself.
If you said 8
, you have the right answer!
All of these components are removed when the powerup
despawns.
You were probably able to see that Body
, PolygonSprite
, ImageSprite
, PolygonCollider
, RecoverSpeed
, DecayTurnRate
and WrapOutsideBoundary
all add components to powerup
.
The one that you may have missed is the on BeforeCollide that { ... }
block, which is also a component!
In Easel, even blocks of code can be components!
These are called behaviors.
Like any other component, behaviors will be terminated when the entity despawns.
We often refer to the current entity using the special name this
.
You will see this
sometimes written in the code, or in the documentation.
For example, click here to see the documentation for PolygonSprite.
See that it is listed as this.PolygonSprite
,
which tells you that it will add a PolygonSprite
to the current entity this
.
It is important to always know which entity this
refers to,
because by default, this
determines the lifespan for any components we create.
Using this knowledge, you can create all kinds of temporary entities in your game,
like projectiles, powerups and explosions.
If you want to learn more, see the documentation on the special variable this
here.
Only one shield
You might notice it is currently possible to pickup multiple shields. There is no point in having more than one shield at a time, and so let's make it so that picking up a shield expires the previous one.
To do this, we are going to use a field. This field will store the previous shield that was added to a ship so we can find it later.
Select your shield.easel
file. Insert the following highlighted code at the very top, above your Shield
function:
field ship.PreviousShield
pub fn shield.Shield(ship) {
// ...
}
A field allows you to store a value on an entity.
Now let's make the shield powerup expire the previous shield.
In your shield.easel
file, within your ShieldPowerup
function, replace the ship.Subspawn shield
block with the code:
pub fn powerup.ShieldPowerup() {
// ...
on BeforeCollide that {
if that.Category.Overlaps(Category:Ship) {
use ship = that
PreviousShield.Expire
PreviousShield = ship.Subspawn shield {
Shield(ship=)
}
Expire
break
}
}
}
Click Preview. You should find that picking up a second shield powerup does not do anything.
PreviousShield.Expire
will simply do nothing if there is no previous shield.
This is different to many other programming languages which would error when they encounter an undefined value.
In general, built-in Easel functions will just ignore non-existent values, rather than raising errors.
Shield lifetime
Let's make the shield only last for a limited time.
In your shield.easel
file, insert the following code into your Shield
function at the bottom:
pub fn shield.Shield(ship) {
// ...
once Tick(5s) { Expire }
}
Click Preview. You should find that the shield disappears by itself after 5 seconds.
How does Expire know which entity to expire?
Just like many other functions, by default it operates on the current entity this
.
Do you remember how to find out which entity this
is?
We can look at the function signature, fn shield.Shield(ship)
to see that the this
is the shield
because it appears before the .
.
Click this link to look at the function reference for Expire.
Notice that it is documented as this.Expire
. The this
tells you that it will act on the current entity this
,
unless you specify otherwise. If you are ever unsure about which entity a function will act on,
you can look at its function reference to see if it says this
or something else.
Indicating shield lifetime
We have the shield expiring after 5 seconds, but we need to do a better job of informing the player of when their time is up.
Delete the once Tick(5s) { Expire }
line that you just inserted into shield.easel
so we can try a different approach.
First, 5 seconds is quite short, so let's make the shield last longer, say 15 seconds.
In shield.easel
, insert the following highlighted code to define a lifetime
variable that we can use later.
pub fn shield.Shield(ship) {
use body=ship, owner=ship.Owner
use radius=2, luminous=1
use lifetime=15s
// ...
}
Now let's make the shield fade out over the course of its 15-second lifetime. We will do this by continuously replacing its sprite with a more faded-out sprite over the course of its lifetime.
In shield.easel
, replace the PolygonSprite
in your Shield
function with the following highlighted code.
pub fn shield.Shield(ship) {
use body=ship, owner=ship.Owner
use radius=2, luminous=1
use lifetime=15s
PolygonCollider(shape=Circle, category=Category:Shield, density=0)
let birth = Tick
with Tick(0.1s) {
let age = Tick - birth
let proportion = (age / lifetime).Clamp(0, 1)
PolygonSprite(shape=Circle, opacity=0.5*(1-proportion), ownerColor=true, bloom=3, shading=0.1, shadow=0.5, layer=-5)
if proportion >= 1 {
Expire
break
}
}
}
Click Preview, catch a shield powerup and you should see it fade out slowly over 15 seconds. The shield currently makes you invincible, so once you catch a shield it should be easy to test this feature.
The with
block defines a block of code that repeats, in this case every 0.1
seconds.
Let's think about what this means for Line 12 - the PolygonSprite.
In a normal programing language, this would keep adding a new sprite every 0.1
seconds,
until the shield is covered in an overwhelming number of sprites.
Easel is different. In Easel, the PolygonSprite
gets replaced each time.
What we are actually doing is replacing the sprite with a new one with a slightly lower opacity
each time.
That is how we animate the shield fading out.
If you would like to know more about how Easel knows which sprite to replace each time, see automatic IDs.
Expire the shield after hit
We want the shield to only last one hit from an asteroid,
otherwise the game becomes too easy.
In your shield.easel
file, insert the following highlighted code into your Shield
function at the bottom:
pub fn shield.Shield(ship) {
// ...
on BeforeCollide that {
if that.Category.Overlaps(Category:Asteroid) {
Expire
break
}
}
}
Click Preview. You should find that the shield disappears after being hit by an asteroid.
Both with
and on
blocks are ways of defining something that should happen when something happens
(for example, our shield collides with an asteroid).
The difference is that with
runs once immediately, and then again on each change,
whereas on
waits for the change to happen first before running.
We use on
here because we want to first wait for the shield to be hit by an asteroid before expiring it.
We had to use with
in the previous section because otherwise the PolygonSprite
would not be shown until after 0.1 seconds.
with
and on
blocks are types of behaviors. See behaviors to learn more.
Announcing the shield
Let's display a message to the player while their shield is active.
We want the message to disappear when the shield expires.
In your shield.easel
file, insert the following highlighted code into your Shield
function at the bottom:
pub fn shield.Shield(ship) {
// ...
BottomLeftContent {
HStack(padding=1) {
Pip(backgroundColor=#dd00ff) { Image(@shield.svg, height=2, shadow=0.5) }
"You are protected by a shield!"
}
}
}
Click Preview to test your game. You should see a message when you pick up the shield powerup, and it should disappear when the shield expires.
How does BottomLeftContent know to disappear when the shield expires?
Once again, it is because it becomes part of the current entity this
, which is the shield
.
When the shield
despawns, all of its components despawn with it.