Abilities
Commonly, games will have multiple abilities that the player can switch between, whether that be different weapons, spells, tools or something else. In this section, we will learn how to do this in Easel by adding a second weapon to our game, and then allowing the player to switch.
Importing assets
Before we get into the coding part, we are going to need some icons for our abilities.
-
Download the following files by right-clicking the links and choosing Save Link As (Google Chrome) or Download Linked File (Safari):
-
Drag-and-drop both files into the
imagesfolder in the file list on the left side of the editor.
These assets were made by Lorc and come from Game-Icons.net, an excellent resource for free game icons!
Bomb ability
Our new weapon will be a bomb that explodes in a radius, damaging all asteroids nearby.
Create a new file called bomb.easel. Copy-and-paste the following code into it:
pub fn projectile.Bomb([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),
turnRate = 2rev * (Random < 0.5 ? 1 : -1),
)
PolygonCollider(
shape=Circle(radius=detonateRadius),
category=Category:Projectile, collideWith=(Category:Tangible ^ Category:Projectile),
intercept=true, parent=unit, isSensor=true,
)
PolygonSprite(shape=, color=, bloom=3, glare=0.25, luminous=1, shadow=0.5)
on Paint(0.1s) {
PolygonSpark(shape=, color=, fade=1, shine=0.25, luminous=1, crater=0.9, dissipate=0.5s)
}
once Tick(3s) { Expire }
once BeforeCollide {
Spark(color=, radius=areaOfEffect, glare=0.1, glareAlpha=0.1, feather=1, luminous=1, dissipate=0.25s)
repeat 5 {
Spark(color=, radius=, splatter=1, speed=10, glare=0.5, bloom=1, feather=1, luminous=1, dissipate=0.5s)
}
for that in QueryWithinRadius(areaOfEffect) {
if that.Category.Overlaps(Category:Asteroid) {
that.ApplyDamage(20 * DamageFactor)
that.ApplyImpulse(impulse * Direction(that.Pos - Pos))
Score += 10
}
}
Expire
}
}
The bomb projectile is defined similarly to the laser projectile, but the key difference is it has an area of effect. We use QueryWithinRadius to find all asteroids within the area of effect so we can apply damage to them and also knock them back.
Choosing abilities
Now we need a way to switch between our two weapons.
We will make a simple interface where the player presses 1 or 2 on their keyboard to select their weapon.
When they click, it will fire the currently selected weapon.
The selected ability
First, we will create a SelectedAbility property to keep track of which ability the player has selected.
Then using this property, we will then create abilities that will listen for clicks,
and fire only when they are the selected ability.
Create a new file called abilities.easel. Copy-and-paste the following code into it:
pub prop unit.SelectedAbility
Laser ability
We will start by creating a LaserAbility.
The LaserAbility will be responsible for activating itself when the 1 key is pressed,
and firing the laser when the player clicks.
Select laser.easel. Insert the highlighted code snippet at the top of the file, before the Laser function:
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) {
Spawn projectile {
Laser
}
await Tick(cooldown / RateOfFireFactor)
}
}
}
pub fn projectile.Laser([unit, owner]) {
// ...
}
Bomb ability
In the same way, we will create a BombAbility that activates the Bomb ability and also fires it.
Select bomb.easel. Insert the highlighted code snippet at the top of the file, before the Bomb function:
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) {
Spawn projectile {
Bomb
}
await Tick(cooldown / RateOfFireFactor)
}
}
}
pub fn projectile.Bomb([unit, owner]) {
// ...
}
Ability system
Now that we have our abilities defined, we will create an AbilitySystem which we can add to a unit to give it abilities.
Because the abilities themselves do most of the work of activating themselves and firing projectiles,
the AbilitySystem is actually quite simple,
Select abilities.easel. Insert the highlighted code snippet at the bottom of the file:
pub prop unit.SelectedAbility
pub fn unit.AbilitySystem([owner]) {
SelectedAbility = Subspawn ability { LaserAbility }
Subspawn ability { BombAbility }
}
Using the AbilitySystem
Now we just need to add AbilitySystem to our ship, and also remove the old code that made the ship fire lasers,
since that will now be handled by the AbilitySystem.
Select ship.easel. Insert the highlighted code snippet into the Ship function:
pub fn unit.Ship([owner]) {
use body=unit
let radius=1
Body(pos=@(0,0))
PolygonCollider(shape=Circle(radius=), category=Category:Ship)
BoundaryWrappingSystem(radius=)
HealthSystem(maxHealth=100)
AbilitySystem
// ...
}
Now, find the old on ButtonDown(Click) code that made the ship fire lasers. It should look like this:
pub fn unit.Ship([owner]) {
// ...
on ButtonDown(Click) {
CommenceGame
while IsButtonDown(Click) {
Spawn projectile {
Laser
}
await Tick(0.5s / RateOfFireFactor)
}
}
// ...
}
Delete the whole on ButtonDown(Click) block and replace it with this instead:
pub fn unit.Ship([owner]) {
// ...
once ButtonDown(Click) {
CommenceGame
}
// ...
}
This way the game will continue to start when the player clicks,
but the firing of the laser will now be handled by the LaserAbility in our AbilitySystem.
Click Preview to test your game.
You should now be able to press 1 and 2 to switch between firing lasers and bombs!
Displaying abilities
There needs to be a way for the player to discover that they have multiple abilities. To do this we will create a simple interface that displays the list of abilities, and highlights the currently selected one.
Creating AbilityIcon
We want all abilities to be displayed in a consistent way,
so first we will create an AbilityIcon function so both abilities can share the same styling.
Select abilities.easel. Insert the highlighted code snippet at the bottom of the file:
pub prop unit.SelectedAbility
pub fn unit.AbilitySystem([owner]) {
SelectedAbility = Subspawn ability { LaserAbility }
Subspawn ability { BombAbility }
}
pub fn AbilityIcon(image, keycode, isSelected, tooltip?, [ui]) {
Pip(keycode, backgroundColor=Color:Primary, tooltip=, zStack=true, height=(isSelected ? 4 : 3), width=height) {
Image(image, stretch=true)
ZOverlay(align=Align:Left, vAlign=VAlign:Bottom, padding=0.25) {
Span(bold=true, fontSize=1.5) { KeyBindingDisplay(keycode) }
}
}
}
A Pip is a special kind of button that is stylized for showing icons.
Ability gallery
Now we need to create the space to display our ability icons,
and for this we will create a new gallery called AbilityGallery.
Let's first create the AbilityGallery, and place it in the bottom left corner of the screen.
Select abilities.easel. Insert the highlighted code snippets:
pub prop unit.SelectedAbility
pub gallery unit.AbilityGallery
pub fn unit.AbilitySystem([owner]) {
SelectedAbility = Subspawn ability { LaserAbility }
Subspawn ability { BombAbility }
CommandBarLeft {
HStack(padding=1) {
AbilityGallery
}
}
}
pub fn AbilityIcon(image, keycode, isSelected, tooltip?, [ui]) {
// ...
}
A gallery is a special kind of user interface element which gets given content from other places in the code.
Laser icon
Now we will make the LaserAbility display itself in the AbilityGallery.
Select laser.easel. Insert the highlighted code snippet:
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) {
Spawn projectile {
Laser
}
await Tick(cooldown / RateOfFireFactor)
}
}
give AbilityGallery {
with SelectedAbility {
AbilityIcon(
keycode=, image=@laser-precision.svg,
isSelected=(SelectedAbility == ability),
tooltip="Press 1 for Lasers",
)
}
}
}
pub fn projectile.Laser([unit, owner]) {
// ...
}
Bomb icon
Now, we will make the BombAbility display itself in the AbilityGallery as well.
Select bomb.easel. Insert the highlighted code snippet:
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) {
Spawn projectile {
Bomb
}
await Tick(cooldown / RateOfFireFactor)
}
}
give AbilityGallery {
with SelectedAbility {
AbilityIcon(
keycode=, image=@fire-bomb.svg,
isSelected=(SelectedAbility == ability),
tooltip="Press 2 for Bombs",
)
}
}
}
pub fn projectile.Bomb([unit, owner]) {
// ...
}
Click Preview to test your game. You should now see two icons in the bottom left of the screen,
one for the laser and one for the bomb.
When you press 1 or 2 to switch between abilities,
you should see the corresponding icon get bigger to indicate it is selected.
Unlocking abilities
In many games, players start with only one ability, and then unlock more as they progress. Sometimes they acquire new abilities by reaching certain milestones, sometimes they need to find them through quests or exploration, and sometimes they can even choose their next ability through a skill tree.
In our game, we will make the bomb ability locked at the start, and then the player can unlock it as one of their upgrades.
Bombs unlocked flag
First, we will make a property called HasUnlockedBombs to indicate whether the player has unlocked the bomb ability or not.
Select upgrades.easel. Insert the highlighted code snippet at the top of the file:
pub prop owner.DamageFactor = 1
pub prop owner.RateOfFireFactor = 1
pub prop owner.HasUnlockedBombs = false
// ...
Using the HasUnlockedBombs flag
Now, we will make use of the HasUnlockedBombs flag in AbilitySystem.
- Select
abilities.easel. - In
AbilitySystem, find the lineSubspawn ability { BombAbility } - Delete it
- Now that it has been deleted, inserted the highlighted code snippet:
pub prop unit.SelectedAbility
pub gallery unit.AbilityGallery
pub fn unit.AbilitySystem([owner]) {
SelectedAbility = Subspawn ability { LaserAbility }
// DELETE this line: Subspawn ability { BombAbility }
with HasUnlockedBombs {
if HasUnlockedBombs {
Subspawn ability { BombAbility }
break
}
}
CommandBarLeft {
HStack(padding=1) {
AbilityGallery
}
}
}
// ...
Unlocking bombs through upgrades
Finally, we just need to unlock the bomb ability when the player chooses the appropriate upgrade.
Select upgrades.easel. Insert the highlighted code snippets into the 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" }
}
}
if !HasUnlockedBombs {
Panel {
HStack {
P { "Unlock Bomb Ability" }
Blank(expand=true)
RaisedButton(PressIntent<upgrade>($wantBombs), 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!" }
},
$wantBombs => {
HasUnlockedBombs = true
PrintUpgradedMsg { "Unlocked bombs!" }
},
}
Expire
}
}
Click Preview to test your game. After you clear the first wave of asteroids, you should see the upgrade dialog appear with a new option to unlock bombs. Only once you click that option should the bomb ability become available in the game.
Recap
In this chapter, you added multiple abilities to your game, and allowed the player to switch between them.
You created a LaserAbility and a BombAbility, with each one listening for the relevant inputs to know when to activate itself
and when to fire projectiles.
Abilities also display themselves into a gallery in the user interface.
As an example, we then made the bomb ability unlockable through an upgrade. This is just one way you could implement unlockable abilities, but there is no reason you couldn't implement other methods of unlocking as well, such as finding them in the world, or unlocking then when the player reaches a certain level or milestone.
In Easel, entities display their own user interfaces, listen for inputs directly, render their own graphics and run their own logic. They do everything themselves! This may seem strange if you are coming from object-oriented programming where there is normally a separation of concerns.