Skip to main content

Powerups

A common game mechanic is powerups, which are items that players can pick up to gain temporary abilities or bonuses. Let's see how we can implement powerups in our game.

On this page, we will add a multi-shot powerup to Astroblast, which gives the player the ability to shoot three projectiles at once for a short period of time.

Importing assets

We are going to need a new image to represent our multi-shot powerup.

  1. Download the following file by right-clicking the link and choosing Save Link As (Google Chrome) or Download Linked File (Safari):

  2. Drag-and-drop the bolt.png file into the images folder in the file list on the left side of the editor.

note

This asset comes from the Space Shooter Remastered asset pack from Kenney, an excellent resource for free game assets!

Creating NumExtraShots

First, we will create a new collector called NumExtraShots to keep track of how many extra shots the player has at any one moment. Later we will make a powerup temporarily give an extra 2 shots to this value, giving them a total of 3 shots at once.

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

pub collect unit.NumExtraShots ~ Sum
info

We are using Sum for the merge function of this collector. This means, if a player is lucky enough to pick up two multi-shot powerups at the same time, they will get 4 extra shots instead of just 2.

See Collectors to learn more about collector merge functions.

Creating the MultiShotPowerup

Now we will create a function MultiShotPowerup for the powerup itself. When the powerup collides with a ship, it will give that ship 2 extra shots for 10 seconds, after which the effect will expire.

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

pub collect unit.NumExtraShots ~ Sum

pub fn unit.MultiShotPowerup(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(@bolt.png, radius=, luminous=1)

once BeforeCollide that {
if that.Category.Overlaps(Category:Ship) {
Spawn {
give NumExtraShots(2)
once Tick(10s) { Expire }
}
}
Expire
}
}

Spawning the MultiShotPowerup

We will once again make the powerup appear randomly when an asteroid is destroyed, in the same way as the ExtraLifePowerup. We will make MultiShotPowerup more common than ExtraLifePowerup, which will stay rare.

  1. Select asteroid.easel.
  2. Find the SpawnAsteroidLoot function.
  3. Replace its contents with the following highlighted code:
// ...

fn SpawnAsteroidLoot(pos) {
if Random < 0.005 {
surprise {
0.2 => Spawn unit { ExtraLifePowerup(pos=) },
0.8 => Spawn unit { MultiShotPowerup(pos=) },
}
}
}
info

The surprise block is a convenient way to randomly choose between multiple options with different probabilities.

Using NumExtraShots

When a ship is doing a multi-shot, each projectile should be fired at a different angle so that they spread out. We will put this logic in a function MultiShot so we can reuse it for both lasers and bombs.

Creating the MultiShot function

Select multiShot.easel. Insert the following highlighted code into it at the bottom:

pub collect unit.NumExtraShots ~ Sum

pub fn unit.MultiShotPowerup(pos) {
// ...
}

pub fn MultiShot(anglePerShot=0.05rev, [unit]) |angleOffset| {
let startAngle = -0.5 * anglePerShot * NumExtraShots
for i in Range(0, 1+NumExtraShots) {
delve(angleOffset = startAngle + i*anglePerShot)
}
}
info

We are implementing MultiShot using Subblocks, which are a special form of Callback. The delve keyword is used to call the subblock, and we can pass parameters into the subblock by declaring them between two pipe characters | in the function signature.

Multi-shot Laser

Now let's modify our LaserAbility function to use MultiShot.

  1. Select laser.easel.
  2. Inside LaserAbility, find the Spawn projectile { Laser } block.
  3. Delete it and replace it with the following highlighted code:
pub fn ability.LaserAbility([unit, owner]) {
let keycode=Digit1, cooldown=0.5s

on ButtonDown(keycode) { SelectedAbility = ability }

on ButtonDown(Click) {
while SelectedAbility == ability && IsButtonDown(Click) {
MultiShot angleOffset {
Spawn projectile {
Laser(angleOffset=)
}
}
await Tick(cooldown / RateOfFireFactor)
}
}

give AbilityGallery {
with SelectedAbility {
AbilityIcon(
keycode=, image=@laser-precision.svg,
isSelected=(SelectedAbility == ability),
tooltip="Press 1 for Lasers",
)
}
}
}

// ...

This will pass the angleOffset from MultiShot into Laser. We need to modify Laser to accept this parameter. We will do that next.

Angled lasers

Select laser.easel. Add angleOffset into the Laser function like this:

pub fn ability.LaserAbility([unit, owner]) {
// ...
}

pub fn projectile.Laser(angleOffset=0rev, [unit, owner]) {
use body=projectile, luminous=1
let speed=50, radius=0.25, color=#00ccff

Body(
pos=unit.Pos,
velocity = speed * Direction(unit.Heading + angleOffset),
)

// ...
}

That's the laser complete, now we need to do the same with bombs.

Multi-shot bombs

  1. Select bomb.easel.
  2. In the BombAbility function, find the Spawn projectile { Bomb } block
  3. Delete it and replace it with the following highlighted code:
pub fn ability.BombAbility([unit, owner]) {
let keycode=Digit2, cooldown=1.25s

on ButtonDown(keycode) { SelectedAbility = ability }

on ButtonDown(Click) {
while SelectedAbility == ability && IsButtonDown(Click) {
MultiShot angleOffset {
Spawn projectile {
Bomb(angleOffset=)
}
}
await Tick(cooldown / RateOfFireFactor)
}
}

give AbilityGallery {
with SelectedAbility {
AbilityIcon(
keycode=, image=@fire-bomb.svg,
isSelected=(SelectedAbility == ability),
tooltip="Press 2 for Bombs",
)
}
}
}

// ...

Angled bombs

Select bomb.easel. Add angleOffset into the Bomb function like this:

pub fn ability.BombAbility([unit, owner]) {
// ...
}

pub fn projectile.Bomb(angleOffset=0rev, [unit, owner]) {
use body=projectile, luminous=1
let speed=10, impulse=20, radius=0.3, color=#00ccff
let shape=Capsule(radius=, extent=0.1)
let areaOfEffect=5, detonateRadius=0.35*areaOfEffect

Body(
pos=unit.Pos,
velocity = speed * Direction(unit.Heading + angleOffset),
turnRate = 2rev * (Random < 0.5 ? 1 : -1),
)
// ...
}

Click Preview to test your game. Destroy some asteroids, and eventually a multi-shot powerup will appear. Pick it up, and you should be able to shoot three lasers or bombs at once. This effect should only last for 10 seconds, after which you will go back to shooting one projectile at a time.

tip

If you are having trouble getting a multi-shot powerup, try increasing the chance of it spawning in SpawnAsteroidLoot (e.g. if Random < 0.1) so it spawns more frequently while you're testing. Don't forget to revert this change afterward though, otherwise your players will be getting multi-shot powerups all the time!

Buff glow

In games, a temporary positive effect is called a buff, and it is common for buffs to come with a glowing visual effect so players can see they are still active. Let's do this with our multi-shot powerup.

Select multiShot.easel. Insert the following highlighted code into MultiShotPowerup:

pub collect unit.NumExtraShots ~ Sum

pub fn unit.MultiShotPowerup(pos) {
// ...

once BeforeCollide that {
if that.Category.Overlaps(Category:Ship) {
Spawn {
use unit=that, body=unit
give that.NumExtraShots(2)
once Tick(10s) { Expire }

PolygonSprite(
shape=Circle(0), color=, luminous=1,
bloom=5, bloomAlpha=1, opacity=0.5, layer=-10,
)
}
}
Expire
}
}

Click Preview to test your game. Destroy some asteroids until you can pick up a multi-shot powerup. You should see a golden glow around your ship while the buff is active.

info

This is an example of a PolygonSprite that is owned by one entity (the buff effect), but is attached to the body is a different entity (the ship receiving the buff). The result of this is the sprite follows the ship around the screen, but expires when the buff expires (after 10 seconds), as opposed to when the ship expires which would be much later.

Recap

In this chapter, we implemented a multi-shot powerup that gives the player the ability to shoot three projectiles at once for a short period of time.

We created a new collector NumExtraShots to keep track of how many extra shots the player has. Our collector has a Sum merge function, which means if the player picks up two multi-shot powerups at once, they will get 4 extra shots instead of just 2.

A new function MultiShot to handle the logic of spawning multiple projectiles at different angles. We implemented this using subblocks which let us reuse the same MultiShot function for both lasers and bombs, even though they spawn different projectiles.

Finally, we added a glowing visual effect to the ship while the multi-shot buff is active. This was an example of a PolygonSprite that is owned by one entity (the buff effect), but is attached to the body of a different entity (the ship receiving the buff).