For Object-Oriented Programmers
Easel is not a object-oriented programming language. This page contains some tips to help object-oriented make the transition to Easel.
Entities are like objects, sometimes
In general, you would spawn an Entity whereever you would use an object in an object-oriented programming language.
For example, if you were making a game with spaceships,
you might have a Ship class in an object-oriented programming language.
You can make a Ship entity in Easel that serves the same purpose:
pub game fn World.Main() {
SpawnEachPlayer owner {
Subspawn unit {
Ship
}
}
}
pub fn unit.Ship([owner]) {
// ...
}
Except when they are Maps
If your object is just a data container with no behavior, then it doesn't need to be an Entity and might be better as a Map instead.
For example, maybe we have some WeaponStats objects that just hold some numbers about how much damage a weapon does,
how fast it fires, etc.
For this, you can just make a Map.
Entities have to be intentionally despawned,
whereas Maps are reference-counted so will automatically be cleaned up when they are no longer referenced.
Entities tick themselves
A typical pattern in object-oriented programming is to have an Update method on your objects that gets called every frame.
In Easel, you would instead have your entity add an on Tick behavior to itself:
pub fn unit.Hero([owner]) {
use body=unit
Body(pos=@(0, 0))
PolygonSprite(shape=Circle(radius=1), color=#00ff00)
on Tick {
Pos += @(1, 0) // move right every tick
}
}
Avoid executing every tick, if you can
Running code every tick can be wasteful if nothing changed.
Consider hooking to more specific events.
For example, if your code only needs to run when the player clicks,
you can hook to ButtonDown(Click) instead of Tick:
pub fn unit.Hero([owner]) {
use body=unit
Body(pos=@(0, 0))
PolygonSprite(shape=Circle(radius=1), color=#00ff00)
on ButtonDown(Click) {
repeat 5 {
Spark(radius=1, color=#00ff00, speed=10)
}
}
}
Methods
In object-oriented programming, you have methods which are actions you can tell your objects to do. Methods normally are verbs like:
ship.FlyTowards(target)hero.PlayWalkingAnimation()laser.FireWeapon()
Functions
Some methods can simply be translated directly into functions that take an entity as a parameter:
pub fn ship.FlyTowards(target) {
ship.Pos += (target - ship.Pos).Truncate(10)
}
State
Methods tend to be verbs. Easel is a reactive programming language which means it is more about state than verbs.
So consider whether instead of telling your hero to "play walking animation", you should set the hero's current animation state to "walking":
pub prop hero.CurrentAnimation = $idle
pub fn hero.Hero() {
use body=hero
Body(pos=@(0, 0))
with CurrentAnimation {
let image = match CurrentAnimation {
$idle => @hero-idle-*.png,
$walking => @hero-walking-*.png,
}
ImageSprite(image=, frameInterval=0.1s, frameRepeat=true, radius=1)
}
}
The benefits of this are more apparent in more complex cases. For example, let's say your hero can pick up an invisibility powerup that makes them invisible for 5 seconds. When the sprite returns, they need to return to their previous animation state, whether that be idle or walking. You will need to have stored this state so you can return to it after the invisibility wears off.
Signals
Abstract methods are often most closely modeled with signals in Easel.
Let's say you are making a game with multiple weapons. This is how you would do it in an object-oriented way:
- Create a
Weaponbase class with an abstract methodFireWeapon(). - Create subclasses
LaserWeapon,RocketWeapon, etc. that inherit fromWeaponand implement theFireWeapon()method differently.
In Easel, we could have a FireWeapon signal that our weapons listen to instead to fire.
pub signal weapon.FireWeapon
pub fn weapon.LaserWeapon([owner]) {
on FireWeapon {
Spawn projectile {
Laser
}
}
}
pub fn weapon.RocketWeapon([owner]) {
on FireWeapon {
Spawn projectile {
Rocket
}
}
}
Eliminating the call altogether
The LaserWeapon example above is not how you would normally do it in Easel.
In Easel, the more natural way is for the LaserWeapon to listen directly for the player input:
pub fn weapon.LaserWeapon([owner]) {
on ButtonDown(Click) {
Spawn projectile {
Laser
}
}
}
This cuts out the abstraction of FireWeapon alttogether.
Perhaps FireWeapon had some useful functionality in it, like applying a cooldown.
If that is the case, in Easel you might put that functionality into a function
with subblock and then call that from LaserWeapon:
pub fn weapon.LaserWeapon([owner]) {
FireWeaponWithCooldown {
Spawn projectile {
Laser
}
}
}
pub fn this.FireWeaponWithCooldown([owner]) || {
on ButtonDown(Click) {
delve()
await Tick(0.5s)
}
}
Notice, the on ButtonDown(Click) handler is still on the LaserWeapon, just within the nesting of FireWeaponWithCooldown.
Nothing else is telling the LaserWeapon to fire still.
This continues the philosophy that entities tick themselves, entities fire themselves,
entities think for themselves in Easel.