Skip to main content

Lives

It is very common in games for the player to have multiple lives, and for the game to end only when they run out of lives. This stops them from losing all their progress when they make a small mistake, making the game more forgiving. Let's learn how to do this in Easel.

NumLivesRemaining property

First we will create a property to keep track of how many lives the player has remaining.

Create a new file called lives.easel. Insert the following highlighted code into it:

pub prop owner.NumLivesRemaining = 1
info

We use a property here because we want to be able to trigger other behaviors when the number of lives changes,

Using the NumLivesRemaining property

Now that we have created our NumLivesRemaining property, we need to use to respawn the player if they still have lives remaining. To do this, we will put our ship spawning code inside a loop so it can run repeatedly until the player runs out of lives.

  1. Select main.easel.
  2. In Main, find the await Subspawn unit { ... } block
  3. Replace it with the following highlighted code:
pub game fn World.Main(maxHumanPlayers=5) {
// ...

SpawnEachPlayer owner {
ScoreSystem
UpgradeSystem

loop {
await Subspawn unit {
Ship
}

if NumLivesRemaining <= 0 { break }
NumLivesRemaining -= 1
await Tick(2s)
}

Eliminated = true

Subspawn dialog {
GameOverDialog
}
}

// ...
}

// ...

Click Preview to test your game. Crash your ship into an asteroid. After your ship is destroyed, you should respawn after 2 seconds.

Displaying the lives remaining

We want to display the number of lives the player has remaining on the screen, so the player knows how many chances they have left before the game is over. To achieve this, we will create a LivesSystem that will display the number of lives remaining on the toolbar.

Creating the LivesSystem

Select your lives.easel file. Insert the following highlighted code into it:

pub prop owner.NumLivesRemaining = 1

pub fn this.LivesSystem([owner]) {
ToolbarRight {
HStack(hPadding=1, gap=0.2) {
Image(@hero.png, height=1)
with NumLivesRemaining {
" × " + NumLivesRemaining
}
}
}
}

We are not yet done, but click preview just to make sure there are no syntax errors.

info

ToolbarRight is a component that displays user interface content on the right side of the toolbar at the top of the screen.

Using the LivesSystem

Now that we have created our LivesSystem, we need to add it to each player.

pub game fn World.Main(maxHumanPlayers=5) {
// ...

SpawnEachPlayer owner {
ScoreSystem
UpgradeSystem
LivesSystem

loop {
await Subspawn unit {
Ship
}

if NumLivesRemaining <= 0 { break }
NumLivesRemaining -= 1
await Tick(2s)
}

Eliminated = true

Subspawn dialog {
GameOverDialog
}
}

// ...
}

// ...

Click Preview to test your game. You should see the number of lives you have remaining displayed at the top right of the screen. Destroy your ship on an asteroid, and you should see the number decrease by one. Keep doing this and once you run out of lives, you should see the game over dialog.

Invincibility frames

When the player revives, sometimes they may revive into the middle of a busy asteroid field full of danger. To give them a fair chance, we will give them a few seconds of invincibility after they respawn, giving them a chance to get out of danger. In games, these are known as "invincibility frames" or "i-frames" for short.

Delaying the collider

To do this, we will delay the creation of the ship's collider for a few seconds after it spawns. Without a collider, the ship cannot collide with anything, so it will be invincible.

  1. Select ship.easel.
  2. Find the PolygonCollider line (near the top)
  3. Delete it and replace it with the following highlighted code:
pub fn unit.Ship([owner]) {
use body=unit
let radius=1

Body(pos=@(0,0))
once Tick(5s) {
PolygonCollider(shape=Circle(radius=), category=Category:Ship)
}

BoundaryWrappingSystem(radius=)
HealthSystem(maxHealth=100)
AbilitySystem

// ...
}

Click Preview to test your game. Fly straight into an asteroid within the first 5 seconds of the game, and you should see that you phase right through it without taking any damage. After 5 seconds though, the asteroid should collide with you and deal damage as normal.

tip

If 5 seconds is not enough to reach an asteroid, try temporarily increasing it to something longer (e.g. 30 seconds) while you're testing.

Showing invincibility

Games normally make their characters flash during their invincibility frames to make it clear to the player when they are invincible and when they are not. Let's do that too!

Select ship.easel. Insert the following highlighted code snippets:

pub fn unit.Ship([owner]) {
use body=unit
let radius=1

Body(pos=@(0,0))
behavior<invincibleFlash> on Paint(0.4s) {
Strobe(fade=0.5, dissipate=0.25s)
}
once Tick(5s) {
PolygonCollider(shape=Circle(radius=), category=Category:Ship)
delete behavior<invincibleFlash>
}

BoundaryWrappingSystem(radius=)
HealthSystem(maxHealth=100)
AbilitySystem

// ...
}

Click Preview to test your game. Crash into an asteroid and watch as you get revived. You should be flashing for a few seconds and be invincible until the flashing stops, giving you a chance to get out of danger.

info

behavior<invincibleFlash> is just a way to give an ID to this particular behavior so we can delete it later.

Acquiring more lives

Now that we have a lives system, let's create a way for the player to collect more lives. To do this, we will create an ExtraLifePowerup that the player can pick up to gain an extra life.

Creating an ExtraLifePowerup

Select lives.easel. Insert the following highlighted code into it:

pub prop owner.NumLivesRemaining = 1

pub fn this.LivesSystem([owner]) {
// ...
}

pub fn unit.ExtraLifePowerup(pos) {
use body=unit
let radius=0.5, speed=2, color=#ffaa00

Body(pos=, velocity = speed * RandomVector)
BoundaryWrappingSystem(radius=)
DrainingSystem(radius=)

PolygonCollider(shape=Circle(radius=), category=Category:Powerup, collideWith=Category:Ship)
PolygonSprite(shape=Circle(radius=1.5*radius), color=, opacity=0.5, luminous=1, bloom=3, bloomAlpha=1)
ImageSprite(@hero.png, radius=, luminous=1)

once BeforeCollide that {
if that.Category.Overlaps(Category:Ship) {
use owner = that.Owner
NumLivesRemaining += 1
}
Expire
}

once Tick(10s) { Expire }
}

If you click Preview now, you will get an "unknown identifier" error because we haven't created the Category:Powerup category yet.

Creating Category:Powerup

Select categories.easel. Insert the following highlighted code snippet at the bottom of the file:

pub tangible category Category:Ship
pub tangible category Category:Projectile
pub tangible category Category:Asteroid
pub tangible category Category:Powerup

Spawning the powerup

Now we need to spawn the ExtraLifePowerup in our game so the player can actually collect it. Let's make it so every asteroid has a small chance of dropping an ExtraLifePowerup when it is destroyed.

Select asteroid.easel. Insert the following highlighted code snippets:

pub fn unit.Asteroid(pos, parent?, division=1) {
// ...

once OutOfHealth {
repeat 12 / division {
Spark(radius=1, screenOffset=radius*Random*RandomVector, glaze=1, luminous=1, color=#fc4, feather=1)
}
if division < 4 {
let parent=unit
repeat 2 {
Spawn unit {
Asteroid(pos=Pos, parent=, division=division+1)
}
}
}

SpawnAsteroidLoot(pos=Pos)
}

// ...
}

fn SpawnAsteroidLoot(pos) {
if Random < 0.001 {
Spawn unit { ExtraLifePowerup(pos=) }
}
}

Click Preview to test your game. Destroy lots of asteroids and eventually you should see an ExtraLifePowerup spawn.

tip

If you're having trouble finding the ExtraLifePowerup, try making the spawn chance higher (e.g. if Random < 0.1) so it spawns more frequently while you're testing.

Draining Category:Powerup

One last thing to complete this task.

Like the asteroids, each wave should wait until all powerups are gone before ending the wave. This makes sure every wave always ends with a clear screen.

  1. Select asteroidField.easel.
  2. In PlayAsteroidRound, find the while QueryAny(filter=Category:Asteroid) { ... } line.
  3. Replace it with the following highlighted code snippet:
pub await fn PlayAsteroidRound(duration=60s) {
await Spawn {
AsteroidFieldSystem
await Tick(duration)
Expire
}
await Spawn {
give DrainingModeEnabled(true)
while QueryAny(filter=(Category:Asteroid | Category:Powerup)) { await Tick }
Expire
}
}

Recap

In this chapter, you implemented a lives mechanic for the player, giving them multiple chances before the game is over. You created a powerup that gives the player extra lives.

You also made it so the player has a few seconds of invincibility after they respawn to give them a chance to get out of danger. To do this, we created a flashing behavior with an ID, and then used that same ID to delete the behavior after a few seconds, stopping the flashing.

info

Most components can take an ID, which you can use to replace or delete them later. For example, this is how you would create, replace then delete an ImageSprite using an ID:

ImageSprite<hero>(@hero.png)
await Tick(1s)
ImageSprite<hero>(@hero-smiling.png)
await Tick(5s)
delete ImageSprite<hero>

See IDs to learn more.