Ship
The most important part of the game is your ship. In this part of the tutorial, we are going to make a simple spaceship that you can control with the mouse.
Spawning a ship
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.
The first thing we need to do is spawn a ship for the player to control.
In main.easel
, modify the code inside pub game fn World.Main
so it looks like this
(delete what was there previously):
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.
- Delete all the lines currently inside the outermost pair of braces
{
and}
so it looks like the snippet below.pub game fn World.Main() {
} - 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!
Like many other programming languages, in Easel, blocks of code are surrounded by curly braces {
and }
.
Normally, programmers use indentation to make it easy to see which block each line belongs to.
This is not required, but is a good practice because it makes it easier to read and understand the code.
If your indentation is wrong, you can press Tab
or Shift+Tab
to
change the indentation of the currently selected lines.
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.
Click the New File button - you will find it at the very bottom left of the 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, color=#00ccff, 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.
The various image, sound and video files that make up a game are often called Assets.
Importing 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.
If you open these files in your browser, you might just see a white screen and wonder what is wrong. The image is there, it is just white like the background of your browser, and so you don't see anything. The image is white so we can programmatically tint the image with any color we want in game. This is a common game development trick.
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.
Ship color
Let's give our ship a nice strong magenta color.
In ship.easel
, find the ImageSprite(@spaceshipBody.svg, ...)
function call
and change the color
parameter from #00ccff
to #dd00ff
.
Your ship.easel
file should now look like this:
pub fn ship.Ship([owner]) {
use body=ship
use radius=7
Body(pos=@(0,0))
ImageSprite(@spaceshipBody.svg, color=#dd00ff, shading=0.25, shadow=0.5)
ImageSprite(@spaceshipWindow.svg, color=#ffffff, shading=0.25)
}
Click Preview again to see your ship's new color. When you're ready, click Exit Preview.
In Easel, colors are represented as hex codes, the same as in other well-known languages like HTML and CSS.
A very quick crash course:
The first two digits represent the amount of red, the next two the amount of green, and the last two the amount of blue.
The digits, however, are hexadecimal (base 16),
and so they go from 0
to 15
(rather than 0
to 9
) with the letters a
to f
being used to represent 10
to 15
.
This means the biggest number we can make with 2 hexadecimal digits is ff
.
The smallest number is still 00
, making the halfway point 80
.
So #ff8000
represents orange, since it is fully red, half green and no blue.
W3Schools has a good tutorial on hex codes if you want to learn more.
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, color=#dd00ff, 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 Easel, we recommend you make the size of your main character 1
,
and then define the size of everything else relative to that.
This will help make it easy to understand how big everything 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 it easy to add movement to our ship in a way that interacts with other entities in a realistic way.
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.
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 other tangible entities.
It is possible to have intangible categories that are only used for filtering and not for collision.
For more detail, see Categories.
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, color=#dd00ff, 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.
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, color=#dd00ff, 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.
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, color=#dd00ff, 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.
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.
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, color=#dd00ff, 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.
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)
}
}
Now let's add the behavior to the Ship
.
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) {
// ...
}
// ...
}
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 control with the mouse. We did this by creating an entity and attaching various things 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. Even experienced programmers are continually looking up the function reference as they work. You can always easily find the reference by clicking the Reference link in the toolbar at the top of this page.