Skip to main content

Upgrades

A common feature in games is the ability to upgrade your character. Often this can be a huge part of the fun of the game. Some players love searching for the best possible build for their character.

In our game Astroblast, we are going to allow the player to upgrade their ship in between waves of asteroids. Upgrading is going to be like another round of the game.

Creating the upgrades

First, we will create two properties - DamageFactor and RateOfFireFactor - that we can use to apply the upgrades to the player's ship. They will begin at 1, and then upgrades will increase their values to make the player's ship more powerful.

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

pub prop owner.DamageFactor = 1
pub prop owner.RateOfFireFactor = 1

Using DamageFactor

We will need to modify the laser projectile to use the DamageFactor.

  1. Select laser.easel.
  2. Inside the once BeforeCollide block, find the line that.ApplyDamage(10)
  3. Replace it with the following highlighted code:
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 * DamageFactor)
Score += 10
}

Expire
}

once Tick(1.5s) { Expire }
}

Using RateOfFireFactor

We will also need to modify the Ship unit to use the RateOfFireFactor.

  1. Select ship.easel.
  2. Inside the on ButtonDown(Click) block, find the line await Tick(0.5s)
  3. Replace it with the following highlighted code:
pub fn unit.Ship([owner]) {
// ...

on ButtonDown(Click) {
CommenceGame

while IsButtonDown(Click) {
Spawn projectile {
Laser
}
await Tick(0.5s / RateOfFireFactor)
}
}

// ...
}

Creating the upgrade interface

Now we need to make an interface so the player can actually choose which upgrade they want to apply after each wave. We will do this in three parts:

  1. First, we will create a basic interface that can show the upgrade options to one player.
  2. Then, because we are making a multiplayer game, we will need to make a system that triggers the upgrade interface for all players, and then waits until everyone is done before starting the next wave.
  3. Finally, once we have a working multiplayer upgrade system, we will go back and add some extra details to make the upgrade interface flow better.

Creating the UpgradeDialog

First, we will create a basic upgrade interface for one player. This will consist of a dialog that shows two buttons, one for each upgrade. When the player chooses an upgrade, the appropriate prop will be modified to apply the upgrade to the player's ship, and the dialog will close.

Select your upgrades.easel file. Insert the following highlighted code into it at the botttom:

pub prop owner.DamageFactor = 1
pub prop owner.RateOfFireFactor = 1

fn dialog.UpgradeDialog([owner]) {
OverlayContent {
H1(animate=Animate:FlyFromTop) { "Asteroid Field Cleared!" }
VStack(animate=Animate:FlyFromBottom) {
Panel {
HStack {
P { "+30% Damage" }
Blank(expand=true)
RaisedButton(PressIntent<upgrade>($wantDamage), backgroundColor=Color:Primary) { "Upgrade" }
}
}
Panel {
HStack {
P { "+25% Rate of Fire" }
Blank(expand=true)
RaisedButton(PressIntent<upgrade>($wantRateOfFire), backgroundColor=Color:Primary) { "Upgrade" }
}
}
}
}

once Pressed<upgrade> choice {
match choice {
$wantDamage => {
DamageFactor += 0.3
},
$wantRateOfFire => {
RateOfFireFactor += 0.25
},
}
Expire
}
}

We are not done yet, so just preview your game to make sure you have not made any syntax errors before moving on.

info

The PressIntent intent and the Pressed signal together let you respond to a button press in a dialog. In this case, we also provide a value with the intent, so we can tell which button was pressed.

Triggering the UpgradeDialog

Now we need to trigger the UpgradeDialog at the right time for all players, then wait until everyone has made their choice before starting the next wave.

To do this, we will add a few more declarations to the code:

  • A new signal, TriggerUpgradeMode, will tell all players to start showing their upgrade interfaces.
  • A new collector, PlayersStillUpgrading, will track which players are still in their upgrade dialog. Once this collector is empty, we know everyone is done and can start the next wave.
  • A new system, UpgradeSystem, will be added to each player. When it receives the TriggerUpgradeMode signal, it will show the UpgradeDialog to its own player, and keep its player in the PlayersStillUpgrading collector as long as the dialog is still open.

Select upgrades.easel again, and insert the following highlighted code just before the UpgradeDialog function that you just added:

pub prop owner.DamageFactor = 1
pub prop owner.RateOfFireFactor = 1

pub signal World.TriggerUpgradeMode
pub collect World.PlayersStillUpgrading

pub await fn PlayUpgradeRound() {
TriggerUpgradeMode

while !PlayersStillUpgrading.IsEmpty {
await PlayersStillUpgrading
}
}

pub fn this.UpgradeSystem([owner]) {
on TriggerUpgradeMode {
await Subspawn dialog {
give PlayersStillUpgrading(owner)
UpgradeDialog
}
}
}

fn dialog.UpgradeDialog([owner]) {
// ...
}

There are still a couple more steps left to make the upgrade system work, so just preview your game to make sure you have not made any syntax errors before moving on.

Adding the UpgradeSystem to each player

Now we will add the UpgradeSystem to each player.

Select main.easel. Insert the following highlighted code into your Main function:

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

SpawnEachPlayer owner {
ScoreSystem
UpgradeSystem

await Subspawn unit {
Ship
}
Subspawn dialog {
GameOverDialog
}
}

// ...
}

// ...

Adding the upgrade round to the game loop

Now we just need to add a call to PlayUpgradeRound in our game loop, so that the upgrade round is played after each wave of asteroids.

Select main.easel. Insert the following highlighted code into your Main function inside the game loop, just after the call to PlayAsteroidRound:

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

let wave = 1
loop {
PrintWaveMsg(wave=)
await PlayAsteroidRound
await PlayUpgradeRound

AsteroidSpeedMultiplier *= 1.1
wave += 1
}
}
// ...

Click Preview. After you clear the first wave of asteroids, you should see the upgrade dialog appear, and you should be able to choose an upgrade!

How to test faster

We are going to be testing the upgrade system a lot, and it is going to get tiring to play through an entire 60-second wave of asteroids just to test the upgrade system, so here is a trick to speed it up.

In asteroidField.easel, find the line pub await fn PlayAsteroidRound(duration=60s) and temporarily change the duration to something much shorter, like 5s:

pub await fn PlayAsteroidRound(duration=5s) {
// ...
}

We must not forget to change it back to 60s when we are done! One way to remind ourselves is to follow a typical programmer convention and put a TODO comment in the code:

pub await fn PlayAsteroidRound(duration=5s) { // TODO: Revert
// ...
}

If this were a live game, we could use the built-in editor search function to find any TODOs before publishing our game to make sure we don't accidentally leave this change in.

There is also a reminder at the end of this page to revert this change, so you should be fine as long as you remember to read through this whole chapter!

info

Use // to add comments in Easel. Comments are ignored by the compiler and are just for developers to leave notes for themselves or other developers.

Confirmation message

Now that we have a working upgrade system, we can go back and add some extra details to make the upgrade interface flow better. To start with, when a player chooses an upgrade, let's show a confirmation message so the player can feel confident their upgrade was applied.

Consistent confirmation message style

To make sure the confirmation message looks the consistent for all types of upgrades, we are first going to create a function PrintUpgradedMsg.

Select upgrades.easel. Insert the following highlighted code just after the UpgradeDialog function:

fn dialog.UpgradeDialog([owner]) {
// ...
}

fn PrintUpgradedMsg([owner]) |use ui| {
PrintBottom(duration=7s) {
InsetPanel(align=Align:Center) {
delve()
}
}
}

Displaying the message

Now let's go back to the UpgradeDialog function and add calls to PrintUpgradedMsg in the appropriate places.

Select upgrades.easel again. Insert the following highlighted code into your UpgradeDialog function:

fn dialog.UpgradeDialog([owner]) {
OverlayContent {
// ...
}

once Pressed<upgrade> choice {
match choice {
$wantDamage => {
DamageFactor += 0.3
PrintUpgradedMsg { "Damage upgraded!" }
},
$wantRateOfFire => {
RateOfFireFactor += 0.25
PrintUpgradedMsg { "Rate of fire upgraded!" }
},
}
Expire
}
}

Click Preview again. Now when you choose an upgrade, you should see a confirmation message at the bottom of the screen.

Upgrade time limit

In a multiplayer game, we do not want one player holding up everyone else by taking too long to choose their upgrade. So we are going to limit the upgrade round to 15 seconds.

So the player is not surprised, we will show a countdown timer in the upgrade dialog so players know how much time they have left to choose their upgrade.

Select upgrades.easel. Insert the highlighted code snippets into your UpgradeDialog function:

fn dialog.UpgradeDialog([owner]) {
const UpgradeTimeLimit = 15s
let deadline = Tick + UpgradeTimeLimit

OverlayContent {
H1(animate=Animate:FlyFromTop) { "Asteroid Field Cleared!" }
VStack(animate=Animate:FlyFromBottom) {
Panel {
HStack {
P { "+30% Damage" }
Blank(expand=true)
RaisedButton(PressIntent<upgrade>($wantDamage), backgroundColor=Color:Primary) { "Upgrade" }
}
}
Panel {
HStack {
P { "+25% Rate of Fire" }
Blank(expand=true)
RaisedButton(PressIntent<upgrade>($wantRateOfFire), backgroundColor=Color:Primary) { "Upgrade" }
}
}

P(fontSize=0.9, color=#fffc, align=Align:Center) {
"Time until next wave: "
with Tick(1s) { %(Round((deadline - Tick) / 1s).Max(0)) }
" seconds"
}
}
}

once Tick(UpgradeTimeLimit) { Expire }

once Pressed<upgrade> choice {
match choice {
$wantDamage => {
DamageFactor += 0.3
PrintUpgradedMsg { "Damage upgraded!" }
},
$wantRateOfFire => {
RateOfFireFactor += 0.25
PrintUpgradedMsg { "Rate of fire upgraded!" }
},
}
Expire
}
}

Click Preview to test your game again. After you clear the wave of asteroids, you should see a countdown timer in the upgrade dialog. If you wait out the full 15 seconds without choosing an upgrade, the game should auto-advance to the next wave without applying any upgrades.

Showing who we are waiting on

Once a player has chosen their upgrade, they have to wait for the other players to choose theirs. Just hanging out in space with nothing to do could make anyone impatient. Let's show which players they are waiting on so they know something is still happening.

To do this, we will first create a function WaitingForPlayersToUpgradeDialog that shows the players we are waiting on, and then we will make it get shown at the appropriate time.

Creating the WaitingForPlayersToUpgradeDialog

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

fn PrintUpgradedMsg() |use ui| {
// ...
}

fn dialog.WaitingForPlayersToUpgradeDialog([owner]) {
BottomContent {
InsetPanel {
P(fontSize=0.9) {
"Waiting for players: "
with PlayersStillUpgrading {
for use index, owner in PlayersStillUpgrading %%{
if index > 0 {
", "
}
PlayerNameDisplay
}
}
}
}
}
}

Showing the dialog

Now we just need to make this dialog appear just after the player has chosen their upgrade, and make it go away once everyone is done.

Select upgrades.easel again. Insert the following highlighted code into your UpgradeSystem function:

pub fn this.UpgradeSystem([owner]) {
on TriggerUpgradeMode {
await Subspawn dialog {
give PlayersStillUpgrading(owner)
UpgradeDialog
}
await Subspawn dialog {
WaitingForPlayersToUpgradeDialog

while !PlayersStillUpgrading.IsEmpty {
await PlayersStillUpgrading
}
Expire
}
}
}

We are going to have to test this one in multiplayer mode. Click Preview, then click the Invite button. Open the invite link in another browser tab to join the game as a second player. Now when one player chooses their upgrade, they should see a "Waiting for players" dialog at the bottom of their screen listing the other player's name.

No upgrades once eliminated

One last thing to make our upgrade system complete.

Currently, the upgrade interface will show for all players, even the ones who have already been eliminated by crashing into an asteroid.

If you want to see this for yourself:

  1. Enter multiplayer mode
  2. Let one player die by crashing into an asteroid
  3. When the upgrade round starts, it will look strange to the dead player because they will see both the game over screen and the upgrade dialog at the same time, both squeezing themselves onto the same screen.

Let's make it so that the upgrade interface only shows for players who are still alive. To do this, we will check the built-in Eliminated property before showing the player the upgrade interface.

Marking a player as eliminated

Select main.easel. Insert the following highlighted code into your Main function:

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

SpawnEachPlayer owner {
ScoreSystem
UpgradeSystem

await Subspawn unit {
Ship
}

Eliminated = true

Subspawn dialog {
GameOverDialog
}
}

// ...
}

// ...
info

Marking a player as Eliminated also tells the engine to stop optimizing the network for them, improving the multiplayer responsiveness for the players still alive.

Checking if the player is eliminated

Select upgrades.easel again. Insert the following highlighted code into your UpgradeSystem function:

pub fn this.UpgradeSystem([owner]) {
on TriggerUpgradeMode {
if Eliminated { break }
await Subspawn dialog {
give PlayersStillUpgrading(owner)
UpgradeDialog
}
await Subspawn dialog {
WaitingForPlayersToUpgradeDialog

while !PlayersStillUpgrading.IsEmpty {
await PlayersStillUpgrading
}
Expire
}
}
}

Click Preview again, and click Invite to test in multiplayer mode once again in another tab. If you let one player die by crashing into an asteroid, then when the upgrade round starts, the player who died should not see the upgrade dialog at all.

Revert testing changes

We're all done with implementing upgrading now, so if you changed your asteroid round duration in PlayAsteroidRound function to make testing faster, this is a good time to change it back to 60s.

Select asteroidField.easel. Revert your PlayAsteroidRound function signature back to the original:

pub await fn PlayAsteroidRound(duration=60s) {

Recap

In this chapter, you added upgrade mechanics to your game!

You created an UpgradeDialog to show the upgrade options to the player and let them choose which upgrade they want. Then you created an UpgradeSystem that shows the upgrade interface to its player at the right moment.

Finally, you created a PlayUpgradeRound function, which uses a signal to tell all players to show their upgrade interfaces, then uses a collector to determine when all players are done before allowing the game to continue.

The upgrade mechanics you made in this chapter could be the basis of all kinds of potential mechanics:

  • A shop system where players can spend points to buy upgrades
  • A skill tree where players can choose different upgrade paths to customize their character
  • A character progression system where players unlock new upgrades as they progress through levels or quests
  • and much more!

While our upgrade interface was triggered by the end of each wave of asteroids, in a different game, any number of triggers could be used. Here are some other ideas of when you could trigger an upgrade interface in a game:

  • When the player reaches certain milestones like score thresholds or time survived
  • When the player completes certain quests like defeating a boss or finding a hidden item
  • You could trigger it by location, like when the player returns to the home base
  • You could even have the upgrade options available at all times from a toolbar button

We hope you have enjoyed this chapter and see how you could experiment with your own upgrade mechanics in your games!