Skip to main content

Health

When we shoot an asteroid with a laser, we want it to take damage and eventually be destroyed. But to make things more interesting, we want it to take a few hits before it is destroyed, so we are going to add a health mechanic to our game.

Creating a health mechanic

Create a new file called health.easel and copy-and-paste the highlighted code snippet into it:

pub prop unit.Health
pub prop unit.MaxHealth
pub signal unit.OutOfHealth
pub signal unit.Hurt

pub fn unit.HealthSystem(maxHealth=100) {
MaxHealth = maxHealth
Health = maxHealth

with Health {
if Health <= 0 {
OutOfHealth
Expire
break
}
}
}

pub fn unit.ApplyDamage(damage) {
if Health > 0 {
Health -= damage
Hurt
}
}

Let's take a look at the key parts of this code snippet to understand how the health mechanic works:

  • Line 1: The most important part is the Health property which will keep track of how much health the unit entity has left.

  • Line 2: The MaxHealth property keeps track of the maximum health of a unit. We will use this to render the proportion of health left, whether it be 0%, 100% or somewhere in between.

  • Line 10: HealthSystem uses a with block to watch the Health property, and when it drops to 0 or below, the unit will be destroyed.

  • Line 12: When the entity is destroyed, HealthSystem sends an OutOfHealth signal. We will use this to trigger explosion effects when an asteroid or ship is destroyed.

  • Line 19: When one entity (e.g. a laser), wants to damage another (e.g. an asteroid), it will call this ApplyDamage function. ApplyDamage first checks if the entity actually has health. In a bigger game with more objects, our laser might hit things that do not have health, like a planet or a wormhole, and it should do nothing in those cases.

  • Line 22: ApplyDamage also sends out a Hurt signal whenever a unit takes damage. We will use this to create hit flash effects when an asteroid or the ship gets damaged.

info

A property (or prop) is a variable that is attached to an entity. It is special because whenever it changes, it sends a signal, which can be used to trigger other parts of our code to run.

Lasers destroying asteroids

Making lasers do damage

When a laser hits an asteroid, it should reduce the asteroid's health by a certain amount, which may cause it to be destroyed. To do this, we will make the laser call ApplyDamage on the thing it hits.

When we make this game multiplayer, we don't want players to hurt each other, so we will also make it so lasers only hurt asteroids.

Switch to laser.easel, and insert the highlighted code snippet into your Laser function, inside the once BeforeCollide block:

pub fn projectile.Laser([unit, owner]) {
// ...

on Paint {
Streak(color=, radius=, dissipate=0.05s, bloom=2, bloomAlpha=1, glare=1, shadow=0.5)
}

once BeforeCollide that {
Strobe(shine=1, dissipate=0.5s)
repeat 5 { Spark(color=, radius=, shine=0.5, splatter=1, dissipate=0.5s) }

if that.Category.Overlaps(Category:Asteroid) {
that.ApplyDamage(10)
}

Expire
}

once Tick(1.5s) { Expire }
}

Now lasers do damage, but we're not done yet because asteroids don't have health. We will do that next.

info

The Overlaps function lets us check if one set of categories has any categories in common with another set.

Giving health to asteroids

Switch to asteroid.easel, and insert the following highlighted code snippets into your Asteroid function:

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

let radius = 3 / division
let speed = 2 * division

let image = match division {
1 => PickRandom(@asteroid-big-*.png),
2 => PickRandom(@asteroid-med-*.png),
3 => PickRandom(@asteroid-small-*.png),
4 => PickRandom(@asteroid-tiny-*.png),
}

Body(
pos=,
velocity = speed * RandomVector,
heading=1rev*Random,
turnRate=0.1rev*SignedRandom,
)
BoundaryWrappingSystem(radius=)
PolygonCollider(shape=Circle(radius=), category=Category:Asteroid)
ImageSprite(image=, radius=, shadow=0.5)
HealthSystem(maxHealth = 100 / division)
}

Click Preview and try shooting some asteroids! They will take 10 hits to destroy, so keep going!

Hit effects

You destroyed an asteroid, but it didn't look very satisfying. Let's add some hit flashes, and some explosion effects for when an asteroid gets destroyed.

Switch to asteroid.easel, and insert the following highlighted code snippets into your Asteroid function:

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

let radius = 3 / division
let speed = 2 * division

let image = match division {
1 => PickRandom(@asteroid-big-*.png),
2 => PickRandom(@asteroid-med-*.png),
3 => PickRandom(@asteroid-small-*.png),
4 => PickRandom(@asteroid-tiny-*.png),
}

Body(
pos=,
velocity = speed * RandomVector,
heading=1rev*Random,
turnRate=0.1rev*SignedRandom,
)
BoundaryWrappingSystem(radius=)
PolygonCollider(shape=Circle(radius=), category=Category:Asteroid)
ImageSprite(image=, radius=, shadow=0.5)
HealthSystem(maxHealth = 100 / division)

on Hurt {
Strobe(growth=0.1, shine=0.5, dissipate=0.25s)
}

once OutOfHealth {
repeat 12 / division {
Spark(radius=1, screenOffset=radius*Random*RandomVector, glaze=1, luminous=1, color=#fc4, feather=1)
}
}
}
info

The Strobe function adds a temporary modifier for all graphics on an entity. We use it here to create a hit flash effect.

Showing health on asteroids

When it takes 10 hits to destroy an asteroid, it is difficult for players to know how many more hits it will take. Without any visual feedback, players may think it is doing nothing and give up before the asteroid gets destroyed.

To fix this, we will indicate the asteroid's health by making it more red as it gets more damaged.

  1. Select asteroid.easel.
  2. Find the existing ImageSprite(image=, radius=, shadow=0.5) line and delete it.
  3. Insert the highlighted code snippet below the HealthSystem:
pub fn unit.Asteroid(pos, parent?, division=1) {
// ...

Body(
pos=,
velocity = speed * RandomVector,
heading=1rev*Random,
turnRate=0.1rev*SignedRandom,
)
BoundaryWrappingSystem(radius=)
PolygonCollider(shape=Circle(radius=), category=Category:Asteroid)
// DELETE this line: ImageSprite(image=, radius=, shadow=0.5)
HealthSystem(maxHealth = 100 / division)

with Health {
let proportion = Health / MaxHealth
ImageSprite(image=, color=proportion.Mix(#f88, #fff), radius=, shadow=0.5)
}

on Hurt {
Strobe(growth=0.1, shine=0.5, dissipate=0.25s)
}

// ...
}

Click Preview and try shooting some asteroids again! You should see them turn red as they get more damaged. Now you know it's working!

info

The with block executes its block of code once upfront, and then again every time any of the properties it references changes. It is used to keep one thing up-to-date with another thing, in this case keeping the color of the asteroid in sync with its health.

Asteroid destroying ship

Giving health to the ship

Now that we have health and damage working for asteroids, let's add it to the ship as well so it can be destroyed by crashing into asteroids.

Switch to ship.easel, and insert the following highlighted code snippets into your Ship function:

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

Body(pos=@(0,0))
ImageSprite(@hero.png, radius=1.5*radius, angleOffset=0.25rev, shadow=0.5)
PolygonCollider(shape=Circle(radius=), category=Category:Ship)

BoundaryWrappingSystem(radius=)
HealthSystem(maxHealth=100)

on BeforePhysics {
// ...
}

with Pointer {
Heading = Angle(Pointer - Pos)
}

on ButtonDown(Click) {
while IsButtonDown(Click) {
Spawn projectile {
Laser
}
await Tick(0.5s)
}
}

on BeforeCollide that {
if that.Category.Overlaps(Category:Asteroid) {
that.ApplyDamage(50)
}
}

on Hurt {
Strobe(growth=0.25, shine=1, dissipate=0.25s)
}

once OutOfHealth {
repeat 5 {
Spark(radius=, color=#ffffff, luminous=1, bloom=3, feather=1, dissipate=0.75s, splatter=1, speed=10)
}
}
}

We are not done yet, but click Preview anyway to make sure you have entered this code correctly and there are no new errors. Next we will make the asteroids do damage to the ship when they collide with it.

info

The repeat block lets you repeat a block of code a certain number of times.

Asteroids doing damage

Select asteroid.easel. Insert the highlighted code snippet into your Asteroid function:

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)
}
}

on BeforeCollide that {
that.ApplyDamage(50 / division)
}
}

Click Preview and try crashing into some asteroids! You should see your ship flash when it gets damaged. Keep crashing into asteroids and your ship will eventually explode in a big shower of sparks!

Showing health on the ship

In a similar way to the asteroids, we want the ship to show how much health it has left by becoming more red as it gets more damaged.

  1. Select ship.easel.
  2. Find the existing ImageSprite line and delete it.
  3. Insert the highlighted code snippet below the HealthSystem:
pub fn unit.Ship([owner]) {
use body=unit
let radius=1

Body(pos=@(0,0))
// DELETE this line: ImageSprite(@hero.png, radius=1.5*radius, angleOffset=0.25rev, shadow=0.5)
PolygonCollider(shape=Circle(radius=), category=Category:Ship)

BoundaryWrappingSystem(radius=)
HealthSystem(maxHealth=100)

with Health {
let proportion = Health / MaxHealth
ImageSprite(@hero.png, color=proportion.Mix(#f88, #fff), radius=1.5*radius, angleOffset=0.25rev, shadow=0.5)
}

on BeforePhysics {
// ...
}

// ...
}

Click Preview and try crashing into some asteroids again. You should see your ship turn red as it gets more damaged.

info

The Mix function linearly interpolates between two values based on a proportion value between 0 and 1.

Subdividing asteroids

Let's make the game a bit more interesting. When a big asteroid is destroyed, we want it to break into smaller asteroids. The smaller asteroids will be faster and more dangerous, but they will also have less health and be easier to destroy.

Select asteroid.easel. Insert the highlighted code snippet into your Asteroid function, inside the OutOfHealth block:

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)
}
}
}
}

// ...
}

Recap

In this chapter, you added a health mechanic to your game, allowing asteroids and the ship to take damage and eventually be destroyed.

At the core of the health mechanic is the Health property, which stores the current health of an entity. When a Laser hits an Asteroid, it calls the ApplyDamage function which reduces the Health of the asteroid by a certain amount.

To indicate to the player how much health a Ship or Asteroid has left, we tint the ImageSprite more red as Health gets lower. This is done using a with block which executes the ImageSprite not just once upfront, but also every time Health changes, keeping the color in sync with its health.

info

HealthSystem is another example of a system in Easel - a function that adds ongoing functionality to an entity. A good system can be reused across different types of entities, just like we did by adding HealthSystem to both the ship and the asteroids.

See Systems to learn more about this important concept.