Skip to main content

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.

info

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.

info

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.

info

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.

info

The await causes our on block to pause for 0.5 seconds, delaying its return back to the top where it would handle the next ButtonDown signal and fire another laser.

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.

info

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 once block. In Laser we used once BeforeCollide because it can only hit with one thing, and then it is consumed.

  • To handle an event every time it happens, use an on block. In Ship we used on ButtonDown(Click) because the player can fire the laser again and again.

  • The with block is another type of event handler. In Ship we used with Pointer to point the ship towards the mouse pointer. This with block runs once upfront, and then repeats every time the Pointer changes, 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.

info

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.