Making a Spaceship
Let's make a cool spaceship that you can fly around the screen!
The beginning of your universe
Select your main.easel
file from the sidebar on the left.
Inside the main.easel
file is the Main
function which is the entrypoint into your game's code.
This is where you set in motion all the entities and behaviors that make up your game.
Like many other programming languages, in Easel, blocks of code are surrounded by curly braces {
and }
.
We want to start fresh, so delete everything inside the Main
function until it simply looks like this:
pub game fn World.Main() {
}
Spawning a ship
When making an Easel game, most of your time will be spent editing Easel code in .easel
files.
Let's start by spawning the ship that the player controls.
In main.easel
, insert the highlighted code snippet below into the pub game fn World.Main
function:
pub game fn World.Main() {
SpawnEachPlayer owner {
Subspawn ship {
Ship
}
}
}
It is best to use copy-and-paste to avoid typos. This is the easiest way to do this:
- From the code snippet above,
select everything from the
S
ofSpawnEachPlayer
on Line 2 up to and including the}
on Line 6. (Do not select the blank spaces before theS
). - Press
Ctrl+C
(Windows) orCmd+C
(Mac) to copy the code. - Switch to the editor.
- Place your cursor at the very end of
pub game fn World.Main() {
on Line 1, after the opening brace{
. - Press
Enter
to create a new line. You will notice the new line is indented automatically. - Press
Ctrl+V
(Windows) orCmd+V
(Mac) to paste the code.
Before you continue, make sure the code looks exactly the same as above. Coding is a very precise operation, and even a small mistake can stop your code working as expected!
Programmers use indentation to make it easy to see which block each line belongs to.
You can press Tab
or Shift+Tab
to
change the indentation of the currently selected lines.
Go ahead - select some lines and press Tab
to indent them one step,
then Shift+Tab
to unindent them again.
Indentation is not for the computer, it's for the humans (you!) so you can read the code more easily.
Get into the habit of pressing Tab
and Shift+Tab
to indent your code -
it will mean less time finding and fixing your mistakes
and more time actually making games!
What this code does
In Easel, everything is about spawning entities and attaching things to them. Let's analyse the code we have just inserted.
- On Line 1, we define our
Main
function, the entrypoint into our game's code. - On Line 2, we are spawning an entity
to represent our player and giving it the name
owner
. - On Line 3, we are spawning an entity to represent our spaceship
and giving it the name
ship
. - On Line 4, we call a
Ship
function to give our ship some behavior, which we will define next.
Defining ship behavior
Now let's define our Ship
function,
which will be in charge of giving the ship some physical properties and drawing it on the screen.
Create a new file called ship.easel
by following these steps:
- Click the New File button in the very bottom left of your editor screen - look for the button with a plus sign (
+
) on it. - Give your new file the name
ship.easel
.
Now copy-and-paste the code below into your new ship.easel
file:
pub fn ship.Ship([owner]) {
use body=ship
use radius=7
Body(pos=@(0,0))
ImageSprite(@spaceshipBody.svg, ownerColor=true, shading=0.25, shadow=0.5)
ImageSprite(@spaceshipWindow.svg, color=#ffffff, shading=0.25)
}
Body is a built-in function that gives an entity a physical position and velocity in the game world. ImageSprite is a built-in function that draws an image at an entity's position.
You might notice that Lines 7 and 8 refer to a couple of files - @spaceshipBody.svg
and @spaceshipWindow.svg
.
The @...
syntax tells Easel that we are referring to files.
We will add these files to our project next.
Importing assets
The various image, sound and video files that make up a game are often called Assets.
Add the images to your project using the following steps:
- Download the following two files by right-clicking each link and choosing Save Link As (Google Chrome) or Download Linked File (Safari):
- Drag-and-drop both files into the file list on the left side of the editor. If you did this successfully, you should see the two new files appear in the file list.
Now click Preview. You should see a ship in the middle of the screen. It won't do anything yet, but we will fix that soon. Click Exit Preview in the top left to return to the editor.
Smaller ship
Our world is soon going to be filled with many things. Let's make the ship smaller so we can fit more things on the screen.
In ship.easel
, find the line that says radius=7
and change it to radius=1
.
Your ship.easel
file should now look like this:
pub fn ship.Ship([owner]) {
use body=ship
use radius=1
Body(pos=@(0,0))
ImageSprite(@spaceshipBody.svg, ownerColor=true, shading=0.25, shadow=0.5)
ImageSprite(@spaceshipWindow.svg, color=#ffffff, shading=0.25)
}
Click Preview. You should see a smaller ship in the middle of the screen. It might look quite small by itself, but don't worry, we will soon add more things to the game!
In a normal Easel games set their main character's size to 1
,
and then define the size of everything else relative to that.
This makes it easy to understand how big everything is.
"How big is that Asteroid? Oh, it's 5 ships, I know how big that is!"
If you are familiar with other programming languages, you may be wondering how the ImageSprite
is affected
by changing radius
even though radius
does not appear anywhere within the ImageSprite
parameter list on Line 7.
The reason this works is, lines beginning with use
declare context variables,
which are automatically passed into any functions that need them.
See Context to learn more about this unique Easel feature.
Movement
Now let's make the ship move! We are going to do this using Easel's built-in physics simulation, which will make the movement natural and realistic.
Adding a Category
First, every physical object needs to be given a category. This will make it possible to later define rules about how different categories interact with each other. Let's define a category for our ship.
Create a new file called categories.easel
.
You can do this by clicking the New File button in the bottom left of the editor (look for a button with a +
on it).
Copy and paste the following code into your new categories.easel
file:
pub tangible category Category:Ship
This category is tangible
because we want it to physically collide with everything else.
For more detail, see Categories.
Giving the ship a collider
Now let's make the ship accelerate when you click the mouse. To do this, we are first going to add a Collider to our ship entity. This gives the ship some mass and a shape, will allow it to collide with other physical objects in the game.
In ship.easel
, insert the highlighted line of code below at the bottom of your Ship
function,
before the closing brace }
at the end.
For neatness, add a blank line before your new block of code, just like in the example below.
pub fn ship.Ship([owner]) {
use body=ship
use radius=1
Body(pos=@(0,0))
ImageSprite(@spaceshipBody.svg, ownerColor=true, shading=0.25, shadow=0.5)
ImageSprite(@spaceshipWindow.svg, color=#ffffff, shading=0.25)
PolygonCollider(shape=Circle, category=Category:Ship, density=1)
}
If you are having trouble getting the indentation to match the example,
don't forget you can press Tab
or Shift+Tab
to change the indentation of the currently selected lines.
Accelerating on click
Next, we want to get the ship to accelerate when a button is pressed. For this, we are going to use a construct called an on block to execute some code each time a ButtonDown or ButtonUp occurs.
In ship.easel
, insert the highlighted code below into your Ship
function,
before the closing brace }
at the end.
To keep things neat,
be sure to insert a blank line before your new block of code,
just like you see in the example.
pub fn ship.Ship([owner]) {
use body=ship
use radius=1
Body(pos=@(0,0))
ImageSprite(@spaceshipBody.svg, ownerColor=true, shading=0.25, shadow=0.5)
ImageSprite(@spaceshipWindow.svg, color=#ffffff, shading=0.25)
PolygonCollider(shape=Circle, category=Category:Ship, density=1)
on ButtonDown(Click) {
behavior<thrust> on BeforePhysics {
const ShipImpulsePerTick = 0.75
ApplyImpulse(ShipImpulsePerTick * Direction(Heading))
}
}
on ButtonUp(Click) {
delete behavior<thrust>
}
}
In Easel, a game is a simulation that is advances one step forward each tick. There are 60 ticks per second. The on BeforePhysics block allows us to run some code before the physics simulation phase of each tick.
Speed limits and decay
Finally, we want to add some speed limits and some turning decay to the ship to stop it getting out of control.
In ship.easel
, insert the highlighted code below into your Ship
function:
pub fn ship.Ship([owner]) {
use body=ship
use radius=1
Body(pos=@(0,0))
ImageSprite(@spaceshipBody.svg, ownerColor=true, shading=0.25, shadow=0.5)
ImageSprite(@spaceshipWindow.svg, color=#ffffff, shading=0.25)
PolygonCollider(shape=Circle, category=Category:Ship, density=1)
LimitSpeed(maxSpeed=30)
DecayTurnRate
on ButtonDown(Click) {
// ...
}
on ButtonUp(Click) {
// ...
}
}
We sometimes may omit some lines for brevity, indicated by ...
.
Without DecayTurnRate, the ship would start spinning forever as soon as it collides into something since nothing would be there to stop it, making our ship hard to control. There is no friction in space, so we're adding some!
Click Preview and try clicking the mouse. Hold the mouse button down and the ship should accelerate. Unfortunately, it will keep accelerating until it leaves the screen, but we can fix that next.
Steering
We need to add some steering to the ship so that it can turn. We will do this by running some code each time the mouse pointer moves.
In ship.easel
, insert the highlighted code below at the bottom of your Ship
function,
before the closing brace }
at the end.
To keep things neat, be sure to insert a blank line before it, just like in the example below.
pub fn ship.Ship([owner]) {
use body=ship
// ...
on ButtonDown(Click) {
// ...
}
on ButtonUp(Click) {
// ...
}
with Pointer {
Heading = Angle(Pointer - Pos)
}
}
with
and on
are two ways of running some code whenever something happens.
For more detail, see Behaviors.
Click Preview and try moving the mouse around while clicking and holding your mouse button. The ship should now turn towards the mouse cursor and accelerate towards it when you are holding the mouse button down. Click Exit Preview when you're ready.
Engine sparks
Our ship would look a lot cooler if we added an engine plume to the back of the ship. This is easy to do using Easel's built-in Spark function.
In ship.easel
, insert the two highlighted snippets of code below into your Ship
function.
pub fn ship.Ship([owner]) {
// ...
on ButtonDown(Click) {
behavior<thrust> on BeforePhysics {
const ShipImpulsePerTick = 0.75
ApplyImpulse(ShipImpulsePerTick * Direction(Heading))
}
behavior<engineSparks> on Paint {
Spark(
color=#ff8800, dissipate=0.5s, radius=0.5*radius,
luminous=1, shine=0.5, bloom=1, layer=-5,
splatter=1, velocity=-25*Direction(Heading))
}
}
on ButtonUp(Click) {
delete behavior<thrust>
delete behavior<engineSparks>
}
// ...
}
Make sure you have inserted both of the highlighted code snippets! The first one starts the engine sparks, while the second one stops the engine sparks.
Click Preview and try flying your ship around. You should see some sparks coming out of the back of the ship when you click and hold down your mouse button.
Ship color
Let's give our ship a nice strong magenta color so it looks even cooler with the engine sparks. Even though our game is currently a singleplayer game, we are going to do this in a way which makes it easy to support multiplayer later on.
Create a new file called players.toml
file as follows.
You can do this by clicking the New File button in the bottom left (look for the +
button).
Make sure your file is called players.toml
and not players.easel
.
Copy-and-paste the following code into your new players.toml
file:
[players]
selfColor = "#dd00ff"
Click Preview and you should see your ship is now a nice strong magenta color.
In Easel, colors are represented as hex codes, the same as in other well-known languages like HTML and CSS. Hex codes are a way of representing colors using a combination of red, green, and blue values. The numbers may look strange because they are hexadecimal (base 16), instead of the usual decimal (base 10) numbers, which means they use not just the normal digits 0-9 but also the letters A-F to represent numbers 10-15. W3Schools has a good tutorial on hex codes if you want to learn more.
Random initial position
Let's make the ship spawn at a random location instead of always in the center of the map. First, let's declare a constant to define how big our map is going to be. This allows us to refer to this value by name from more than one place in our code.
Create a new file called boundary.easel
. You can do this by clicking the New File button in the bottom left of the editor.
Copy-and-paste the code below into your new boundary.easel
file.
pub const BoundaryRadius = 40
Now go back to ship.easel
and replace the Body(...)
line in the Ship
function as follows:
pub fn ship.Ship([owner]) {
use body=ship
use radius=1
Body(pos = BoundaryRadius * Random * RandomVector)
ImageSprite(@spaceshipBody.svg, ownerColor=true, shading=0.25, shadow=0.5)
ImageSprite(@spaceshipWindow.svg, color=#ffffff, shading=0.25)
// ...
}
Random is a built-in function that generates a random number between 0 and 1,
and is one you will find yourself using often.
BoundaryRadius * Random * RandomVector
is an example of an expression - a
series of operations (in this case, multiplications) that are evaluated to produce a result.
It is like an equation in maths.
Games are full of expressions like this. Games are just math brought to life!
Now click Preview again. You should see your ship spawn at a random location within the boundary of the map. Try exiting and re-entering the preview to see it spawn in different places. When you're ready, Exit Preview and let's move on to the next step.
Wrapping the world
When the ship leaves the boundary of the world, we want it to reappear at the other edge of the screen. This will be important when we have other objects in the game as it will force the player to deal with them eventually.
Defining WrapOutsideBoundary
We are going to define a WrapOutsideBoundary
function
that allow us to give this behavior not just to the ship, but to other entities too later on.
Select your boundary.easel
file, then insert the highlighted code below at the bottom of the file.
To keep things organized, don't forget to insert a blank line before your new WrapOutsideBoundary
function,
just like the example.
pub const BoundaryRadius = 40
pub fn this.WrapOutsideBoundary([body, radius]) {
on BeforePhysics {
let x = body.Pos.X
let y = body.Pos.Y
if body.Pos.X.Abs > radius + BoundaryRadius && body.Velocity.X.Sign == body.Pos.X.Sign {
x = -x
}
if body.Pos.Y.Abs > radius + BoundaryRadius && body.Velocity.Y.Sign == body.Pos.Y.Sign {
y = -y
}
Pos = @(x, y)
}
}
Give the ship the WrapOutsideBoundary
behavior
Now let's make our ship use the WrapOutsideBoundary
function we just defined.
In ship.easel
, insert the highlighted code below into your Ship function:
pub fn ship.Ship([owner]) {
// ...
PolygonCollider(shape=Circle, category=Category:Ship, density=1)
LimitSpeed(maxSpeed=30)
DecayTurnRate
WrapOutsideBoundary
on ButtonDown(Click) {
// ...
}
// ...
}
Show the boundaries to the player
Let's adjust the camera and background color to make it clear to the player where the boundary of the world is.
Select your main.easel
file. Insert the highlighted code at the very top of your Main
function,
immediately after its opening brace {
but before everything else in its function body.
pub game fn World.Main() {
SolidBackground(color=#181122)
Camera(@(0,0), radius=BoundaryRadius, aspectRatio=1)
// ...
}
Click Preview to test it out. Try flying your ship into the boundary. You should find it reappears on the other side. When you're ready, click Exit Preview.
Function reference
We have now created a spaceship that you can fly around the screen with the mouse! We did this by creating an entity and attaching various components to it. This was done by writing the code to call various functions, and we even defined a couple of our own functions. A big part of programming is choosing the right functions to call at the right time. Don't worry if you don't understand all the functions we used in this tutorial - there is no need to memorize them! A full comprehensive list of all functions is always available in the Function Reference. You can always easily find the reference by clicking the Reference link in the toolbar at the top of this page.
Even experienced programmers are constantly looking up the function reference as they work! It is normal to just keep it open all the time in a separate tab, and continually refer to back to it as you go.