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
Healthproperty which will keep track of how much health theunitentity has left. -
Line 2: The
MaxHealthproperty keeps track of the maximum health of aunit. We will use this to render the proportion of health left, whether it be0%,100%or somewhere in between. -
Line 10:
HealthSystemuses awithblock to watch theHealthproperty, and when it drops to 0 or below, theunitwill be destroyed. -
Line 12: When the entity is destroyed,
HealthSystemsends anOutOfHealthsignal. 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
ApplyDamagefunction.ApplyDamagefirst 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:
ApplyDamagealso sends out aHurtsignal whenever aunittakes damage. We will use this to create hit flash effects when an asteroid or the ship gets damaged.
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.
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)
}
}
}
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.
- Select
asteroid.easel. - Find the existing
ImageSprite(image=, radius=, shadow=0.5)line and delete it. - 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!
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.
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.
- Select
ship.easel. - Find the existing
ImageSpriteline and delete it. - 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.
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.
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.