Firing Lasers
Now that we have a ship that can fly around our little universe, let's give it some lasers!
Defining a laser
Create a new file called laser.easel and copy-and-paste the highlighted code snippet into it:
pub fn projectile.Laser([unit, owner]) {
use body=projectile, luminous=1
let speed=50, radius=0.25, color=#00ccff
Body(
pos=unit.Pos,
velocity = speed * Direction(unit.Heading),
)
PolygonCollider(
shape=Circle(radius=),
category=Category:Projectile,
intercept=true,
)
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) }
Expire
}
once Tick(1.5s) { Expire }
}
If you try to Preview this code now, it will give you an "unknown identifier" error
because we are referring to a Category:Projectile that we have not yet defined.
We will fix that in the next section.
A once block is a component that waits for a signal
then executes its block of code once.
It differs from an on block which can trigger repeatedly.
Streak creates a special type of particle effect that traces the line or curve of a moving object. Perfect for lasers, lightning, streams of fire, tire tracks and more!
Adding a category
Every physical object needs a category so the physics engine can determine what should collide with what.
Switch to categories.easel. Insert the highlighted code snippet at the bottom:
pub tangible category Category:Ship
pub tangible category Category:Projectile
Firing the laser
Now we have a Laser function and a Category:Projectile, we can make our ship fire lasers!
Switch to ship.easel. Insert the highlighted code snippet at the bottom of the 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)
on BeforePhysics {
// ...
}
with Pointer {
Heading = Angle(Pointer - Pos)
}
on ButtonDown(Click) {
Spawn projectile {
Laser
}
}
}
Click Preview and try clicking the mouse button to fire lasers from your ship. You'll immediately notice something is wrong. There are sparks coming out of our ship when there should be lasers.
ButtonDown is a signal that triggers when a button is pressed down. See Keycodes for a list of all the buttons you can use, including mouse buttons, keyboard keys, gamepad buttons and more.
Parenting the lasers
A common problem in games is projectiles expiring immediately because they spawn touching the object they are spawned from. This causes them to immediately collide and therefore expire straight away. This is so common it deserves its own section.
Easel has a built-in solution to this.
Setting the parent parameter of PolygonCollider
tells Easel to ignore collisions with that parent,
but only until the child has cleared (is no longer touching) the parent.
Switch back to laser.easel, find your PolygonCollider call, and
add a new parent=unit parameter to it, like so:
pub fn projectile.Laser([unit, owner]) {
use body=projectile, luminous=1
let speed=50, radius=0.25, color=#00ccff
Body(
pos=unit.Pos,
velocity = speed * Direction(unit.Heading),
)
PolygonCollider(
shape=Circle(radius=),
category=Category:Projectile,
intercept=true,
parent=unit,
)
// ...
}
Now click Preview again and try firing your lasers. It should work!
Cooldown
To add some strategy to our game, we want to limit the laser firing to once every 0.5 seconds.
This way, players have to make each shot count instead of just spamming lasers everywhere.
Switch to ship.easel. Insert the highlighted code snippet into the Ship function:
pub fn unit.Ship([owner]) {
// ...
on ButtonDown(Click) {
Spawn projectile {
Laser
}
await Tick(0.5s)
}
}
Click Preview and try firing your lasers again. You should find that you can only fire once every half second.
Repeatedly firing lasers
Our lasers are a bit tricky to use now. Sometimes when you click, they fire, but other times they don't because they are still on cooldown. It is hard to know the right moment to click to fire a laser!
To make this easier and more fun, let's allow players to click and hold to fire a continuous stream of lasers.
Select ship.easel. Find the on ButtonDown(Click) { ... } block and replace all of its contents with
the following highlighted code snippet:
pub fn unit.Ship([owner]) {
// ...
on ButtonDown(Click) {
while IsButtonDown(Click) {
Spawn projectile {
Laser
}
await Tick(0.5s)
}
}
}
Click Preview to test this out. Click and hold, and you should see a steady stream of lasers firing from your ship! Release the button and the lasers should stop firing.
The while loop repeats a block of code as long as a condition is true.
Recap
In this chapter, you made it so your ship can fire lasers when you click the mouse button.
You did this by adding an event handler on ButtonDown(Click) to your ship,
which spawns a Laser projectile when the player clicks.
Lesson: Event-driven programming
Easel is an event-driven programming language. That means that your code runs in response to events, like a mouse click, a collision, or a certain amount of time passing. This way of programming can often be very natural for games.
There are three types of event handlers in Easel, and we have already seen examples of all three:
-
To handle an event once, use a
onceblock. InLaserwe usedonce BeforeCollidebecause it can only hit with one thing, and then it is consumed. -
To handle an event every time it happens, use an
onblock. InShipwe usedon ButtonDown(Click)because the player can fire the laser again and again. -
The
withblock is another type of event handler. InShipwe usedwith Pointerto point the ship towards the mouse pointer. Thiswithblock runs once upfront, and then repeats every time thePointerchanges, allowing the ship's heading to always be in sync with the pointer's position.
Collectively, the on, once and with blocks are known as behaviors in Easel,
and you will be using them everywhere in your games.
Behaviors are more than just event handlers. They are also components that live on entities. When the entity despawns, all its behaviors get removed along with it, avoiding a whole class of bugs related to dangling event handlers.