Skip to main content

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.

info

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.

tip

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.

info

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) {
// ...
}
info

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.

info

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.

info

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.

info

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.

info

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.

info

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.