Learn
Let's check out other elements we can add with Phaser Editor and take a longer look at JavaScript
Hey hi!
Welcome back! I hope you enjoyed the previous tutorial. I wanted to keep going and add something to make it an actual game or at least something that you can interact with.
For our game, Fruit Shoot
, I am going to interpret the Shoot
part very, very liberally. So liberally in fact that you might as well replace it with Drop
. Except we are not going to do that because that would involve changing a bunch of things, so let's just agree between ourselves that shoot actually means drop.
Side-note: The phrase "naming things is hard" is very famous (and true) in software development and more often than not, projects will evolve in a way that, at some point, their original name won't reflect their contents anymore. But that's ok!
With that lengthy explainer out of the way, let's define our goals for this tutorial:
- add new assets for a player character and an apple
- create a level where the player can pop out from behind bushes
- implement player behavior (the right arrow will make player pop out from the next bush)
- create a system where fruit (the apple) is dropped from the top of the screen
- detect when player "catches" the fruit
Let's get to it!
New assets
Say hello to our new assets, apple.png
and hedgehog.png
.
Feel free to download them (right click and Save image as
) and then let's add them to the game. Once again, to add it to the game, we'll choose a directory (or folder) in the Files
section. The previous assets lived in the assets
directory so let's put these there too. Let's make sure it's selected, then right click, then select Upload files
.
In the next window, click Browse
, find the files on your computer, and then select Upload 2 files
. And remember, once they are added, click on Add to asset pack
in the Inspector
on the right.
You can now drag them into the scene to see how they look!
And with that, we can consider objective one complete. Now let's make the level.
Level layout
For the level, feel free to implement your own design. What I was thinking was to have 4 dark bushes on the ground and a few lighter bushes in the foreground. I'll copy and paste the dark_ground_blog
game object 2 more times until I have four. Then I will adjust each of them until I get them to a position I'm happy with. Finally, I will position the lighter bushes in front of them. At the end, I was left with something like this:
You will notice that I also deleted the clouds because I assume they might get into the way later. Step two done! It sure is nice to have small attainable goals. The next one however might be more of a challenge.
Creating the player movement
If you didn't drag the hedgehog
image into the scene before, you can do so now. Also, we want to make it appear from behind the bushes, so we want to make sure its drawn first (and all the other things on top of it). To do that, select it in the Outline
section and press PgDn
(PageDown
) until it's in the very bottom. Now let's look at the code side of things.
To get started, double click on FruitShootMain.ts
in the Files
section. In the last tutorial we added a tween effect for our clouds. It was at the bottom of the file. We don't actually need that so we can go ahead and delete it. The create
function should now look like it did before:
create() {
this.editorCreate();
}
Now that we have cleaned up a bit, let me explain what I want to do. I want to get all the 4 dark bushes in our scene, or rather I want their locations. I will then put the hedgehog game object on the location of the first dark bush. Then I will create some input detection so that when the right arrow is pressed, the hedgehog will appear behind the next bush.
To "get all the 4 dark bushes" in the scene, we need to find the variables that they have been assigned to. If you scroll up in the file, you'll find a bunch of statements like:
const dark_ground_blog = this.add.image(295, 535, "blob");
The dark_ground_blog
part will be the same as the name of the game object in the Outline
section! My game objects happened to have these names (also notice how I mistyped blob
as blog
one time and I'm still stuck with it):
This is the result of Phaser Editor converting what you see in the editor into code.
Another thing we need to do is to change the scope of these variables. Select all the dark bushes (hold ctrl
to select multiple objects) and then change the Scope
value to CLASS
in the inspector.
This makes it so that these variables are on the instance of this class. This is important for us because we want to use them not only where they are defined by Phaser Editor, but also in our create
function.
Now I will put these into something called an array
. Arrays are a data structure in JavaScript which are used to hold a collection or group of items. We actually used arrays before as well, when we did the thing with the clouds! As a result, my create
function looks like:
create() {
this.editorCreate();
const bushes = [this.dark_ground_blog, this.dark_ground_blog_1, this.dark_ground_blog_2, this.dark_ground_blog_3]
}
Now I'll make a new and empty array
const positions = []
And finally we will iterate over the array of bushes and add a position for each of them.
const positions = []
bushes.forEach(b => {
positions.push({x: b.x, y: b.y})
})
bushes.forEach
takes every member of the array, one at a time, and calls a function with it. The function that it calls is in the parenthesis that come next. In that function, the current member of the bushes
array will be the variable b
. The bush has an x
and a y
which indicate their position in the game world. We want to store this position for later. To do that, we push to (add to the end of) the positions
array an object
with those properties
. An object
is like other variables, but instead of having a value, it has keys
or properties
. So in our case, the whole object is:
{x: b.x, y: b.y}
The x
and y
are properties which each have their own values. In this example, they are the x
and y
values of the bush (and yes, b
is an object as well).
By the end of this, the positions
array will hold all the positions of the bushes. Let's put this to the test. Let's take our hedgehog
image (and make sure the Scope
is set to CLASS
as well) and set its position to the first position in the array.
The entirety of the create
function now looks like:
create() {
this.editorCreate();
const bushes = [this.dark_ground_blog, this.dark_ground_blog_1, this.dark_ground_blog_2, this.dark_ground_blog_3]
const positions = []
bushes.forEach(b => {
positions.push({x: b.x, y: b.y})
})
const first_position = positions[0]
this.hedgehog.x = first_position.x
this.hedgehog.y = first_position.y
}
The first_position
is the 0th element of the array because in programming you start counting from 0.
When you run the scene, no matter where the hedgehog was before, it should now be at the first bush.
Add input
Now let's set up the input so that we can move the hedgehog. Phaser Editor can help us here as well. The Scene Editor is not only for things that appear in the game visually, it can also be used to set up physics, collisions, inputs, and more. Phaser Editor lets us set up some keys and then we can go into the code and associate those keys with events, like key presses.
To add a key, right click anywhere in the Scene Editor or the Outline section and hover over Add Object
, then Input
, then Keyboard.Key
.
This will add a keyboard.key
game object in the Outline
section, under Input
(as opposed to Scene
). When you select, you'll see in the Inspector
that there is a new sub-section, called Keyboard Key
. Under that, you'll find Key Code
which allows you to select which key on your keyboard this key (as in, the GameObject) corresponds to. It's a bit confusing because there is a physical key on your keyboard and a "virtual" key in Phaser Editor and they are both called the same thing, but I hope you are still with me.
As you'll see, I have already renamed my game object to RightKey
and I have changed the Key Code
to RIGHT
, associating this game object with the right
arrow on my keyboard.
I'll save (Ctrl S
or Cmd S
on Mac) and open the scene in the code editor. I scroll down a little bit to find a variable called rightKey
.
That's not quite the same as what I had in the Scene editor, where it's called RightKey
, but I can understand why Phaser Editor made this change. In JavaScript, variable names usually start with lowercase letters and capital case words are often used for classes instead. I won't get into what's a class
right now, I just wanted to clear up any potential confusion.
I will now go to the create
function that I was editing earlier and add this code:
let current_position_index = 0
this.rightKey.on('down', () => {
current_position_index = current_position_index + 1
if(current_position_index > positions.length - 1){
current_position_index = 0
}
const new_hedgehog_position = positions[current_position_index]
this.hedgehog.x = new_hedgehog_position.x
this.hedgehog.y = new_hedgehog_position.y
})
That's a lot but let's break it down. First, I create a variable called current_position_index
and set it 0. This will keep track of where we are in the positions array, i.e which element we currently have selected.
Then we add an eventListener
to our rightKey
. That part where it says down
means that it will react to the key being pressed down. Then there is a function which is a section of code that will be executed each time this event (key being pressed down) happens.
In this function, we set the current_position_index
to be one more than what it was before. Then, we immediately check if the new value is within the range of possible positions. This is an if
statement which means that if the condition
in the parentheses is true
, then the code in the next block (between the {}
bits) will run. In the condition, you'll see that I compare the current_position_index
to the length of the array positions
(length meaning how many elements it includes), minus one. The minus one is important because when we are getting an element from the array, the convention in programming is to start counting from 0. But when we think about the length, we start counting from 1. So the length of the positions array is 4, but if I try to access this index (positions[4]
) then I get an error. Because it would look for the fifth element since it starts from 0.
I hope that makes sense! If not, give it some time, that's ok.
After the if
statement, I create a variable to get the relevant position from the positions
array. And finally, I set the hedgehog's x
and y
to that position's x
and y
, like we did before.
And now if you play the scene, and you press the right
arrow on your keyboard, the hedgehog should jump to the next position. And after the last one, it should start over. Yay! Just in case, here's a screenshot of what this part looks like for me.
Add a falling apple
Next, I want to add an apple that falls from the sky, precisely above one of the positions. For that, I'll need physics and physics requires a bit of setup. In my Files
section, I'll find the main.ts
file and open it. I'll scroll down to where it says const game
and add some physics setup:
physics: {
default: 'arcade',
arcade: {
gravity: {
y: 98
}
}
},
Here's what it looks like after these changes:
This should be enough! Let's go back to our scene and add an apple. From the Files
section, I'll drag the apple asset to the Scene editor. I'll select it and move it to the top of the screen. Finally, I want to add a physics body. To do this, I need to have the apple game object selected and then right click on the Scene editor or on top of the item in the Outline section, go down to Arcade Physics
and then select Add body
.
It may look like nothing happened but if you look at the Inspector
on the right, there's a whole bunch of new sections. So let's hit play and see what happens. If everything's alright, the apple should gently glide down the screen.
Now I want to position the apple at one of the positions that we already defined before. Before that, I need to do the familiar step of making the apple's scope be CLASS
(Select the apple game object in the Outline
section, then on the right in the Inspector
, set Scope
to CLASS
). And then let's open the scene in the code editor.
In the same place I set the hedgehog's position, I'll set the apple's position, but with one difference. I'll change its x
value to the value of the position's x
value, but the y
can be 0
. 0 is the top of the screen because in computer graphics, 0, 0
is the top left of the screen (as opposed to the center, which may be what's intuitive for you).
If you play the scene, the apple should now appear on top of the hedgehog.
Configure the collider
The final step to make the hedgehog able to "catch" the apple is to set up collisions. Collision detection is the process of checking if one or more objects are overlapping in a given simulation, physics system, game, or any other such application. To do this, let's add a physics body
to the hedgehog as well.
To do this, repeat the process that we did for the apple. Have the hedgehog game object selected, right click in the Scene Editor
or on the item in the Outline
section, hover over Arcade Physics
, and then choose Add body
. Then let's go to the Inspector
, scroll down, and untick Moves
. We don't actually want the physics system to move this game object around, we just want the physics system to know about its position in the game world. That's why we added the body
.
And finally, let's set up the collider itself.
In the Scene Editor
or in the Outline
section, right click, hover over Add Object
, then Arcade
, then Collider
.
In the Outline
section, there will now be a Arcade
subsection, with a collider
object. Select this and turn your attention to the Inspector
.
There are some familiar things (like having to set the Scope
to CLASS
) and a few new options. In the Collider
sub section, set the Object 1
to hedgehog
, Object 2
to apple
and type in this.collide
to the Collide Callback
section. And also be sure to tick the box next to Overlap Only
.
As a result of this, we have told Phaser that whenever the object that is set to Object 1
overlaps with the object specified in Object 2
, call the function this.collide
. A function is a block of code to be executed so this allows us to react to the collision and perform actions as a result.
Before we try to see what happens, we need to make sure we actually have a function called this.collide
. In the FruitShootMain.ts
code file, scroll down to after the create
function, and add:
collide = (a, b) => {}
Just in case, my entire user code looks like:
/* START-USER-CODE */
// Write your code here
create() {
this.editorCreate();
const bushes = [this.dark_ground_blog, this.dark_ground_blog_1, this.dark_ground_blog_2, this.dark_ground_blog_3]
const positions = []
bushes.forEach(b => {
positions.push({x: b.x, y: b.y})
})
const first_position = positions[0]
this.hedgehog.x = first_position.x
this.hedgehog.y = first_position.y
this.apple.x = first_position.x
this.apple.y = 0
let current_position_index = 0
this.rightKey.on('down', () => {
current_position_index = current_position_index + 1
if(current_position_index > positions.length - 1){
current_position_index = 0
}
const new_hedgehog_position = positions[current_position_index]
this.hedgehog.x = new_hedgehog_position.x
this.hedgehog.y = new_hedgehog_position.y
})
}
collide = (a, b) => {}
/* END-USER-CODE */
You may be thinking what happened to the this
. The this
is quite a complicated topic in JavaScript, but for now, we can think of this
as the "parent entity". So in our case it will be the scene that the code is in so it's basically implied that the this
is there. Again, this is quite advanced so it's OK if you don't fully grasp this now.
Now the scene should run, but nothing will happen. That's because the function is empty. We have to decide what we want to do. I think the easiest and clearest way to show that the hedgehog "got" the apple is to create a score counter.
Add a score counter
Back in the Scene Editor
, I'll right click anywhere, hover over Add Object
, then go to String
, then click on Text
. This adds a text object to the scene. The reason that it's under the String
sub-menu is that string
is the way programs refer to text. The idea behind the name is that it's a string of characters.
With the new text selected, I'll go to the Inspector
and write Score:
in the Text
in the Text content
sub-section. I also increased the font size but that's completely up to you.
I will then create another text object. This time, I will write 0
in the Text
field and set the same font size.
Finally, I will move them to the top right of the screen and make sure the Scope
value for both of them is set to CLASS
. Finally, let's look at the Outline
section and see which game object corresponds to the text that holds 0
. In my case, it's called text_2
.
Now we can go to the code. Let's make our collide
function look like this:
collide = (a, b) => {
this.text_2.text = Number(this.text_2.text) + 1
const random_index = Phaser.Math.Between(0, 3)
const random_position = positions[random_index]
b.x = random_position.x
b.y = 0
}
Here we want to modify the text value of text_2
. Specifically, I want it to be 1 more than what it was before. This sounds simple, but there is one catch. In JavaScript, if I add (using the +
sign) two strings then they are combined (in programming this is called concatenation
). So 1 + 1
would become 11
. If one part is a string and the other part is a number, the number is automatically converted into a string as well. This is what is happening for us. We need to make sure the current value of this.text_2
is also a number, that's why it's in this Number()
wrapper.
Finally, we re-position the apple
to a new position at the top of the screen. We want it to be a random position so let's choose a random number between 0 and 3. Thats done here:
const random_index = Phaser.Math.Between(0, 3)
Then, we take a random element from the positions
array that we made way back in the create
function.
const random_position = positions[random_index]
And finally we set the x
of the b
(which is the fruit) to that position and its y
to 0.
But don't run the project yet! There is one more thing we have to change. The same positions
array is currently only available in the "scope" of the create
function. That means that anywhere outside the curly brackets of the create
function, it doesn't exist. To make it available outside as well, we need to attach it to the scene that we are in. To do that, at the end of the create
function we have to add:
this.positions = positions
The this
in this case is the scene so we are adding the positions
property to it. And then, since the collide
function is still looking for a thing called positions
(which is not available), we have to change positions[random_index]
to this.positions[random_index]
.
Just in case, the whole code that I added is here. Remember, the variable names might be different, but the logic is here.
/* START-USER-CODE */
// Write your code here
create() {
this.editorCreate();
const bushes = [this.dark_ground_blog, this.dark_ground_blog_1, this.dark_ground_blog_2, this.dark_ground_blog_3]
const positions = []
bushes.forEach(b => {
positions.push({x: b.x, y: b.y})
})
const first_position = positions[0]
this.hedgehog.x = first_position.x
this.hedgehog.y = first_position.y
const random_index = Phaser.Math.Between(0, 3)
const random_position = positions[random_index]
this.apple.x = random_position.x
this.apple.y = 0
let current_position_index = 0
this.rightKey.on('down', () => {
current_position_index = current_position_index + 1
if(current_position_index > positions.length - 1){
current_position_index = 0
}
const new_hedgehog_position = positions[current_position_index]
this.hedgehog.x = new_hedgehog_position.x
this.hedgehog.y = new_hedgehog_position.y
})
this.positions = positions
}
collide = (a, b) => {
this.text_2.text = Number(this.text_2.text) + 1
const random_index = Phaser.Math.Between(0, 3)
const random_position = this.positions[random_index]
b.x = random_position.x
b.y = 0
}
/* END-USER-CODE */
This leaves a funny bug where the fruit gets faster and faster each time. I leave it as a challenge to you to figure out why that is.
And that's it for now! You should really be proud for making it this far. Even though it might seem like "just following instructions", it takes a lot of focus and attention to get all the details right.
To really make this knowledge stick, try and experiment on your own and change things around! Or try to start from scratch and see if you can recreate this. When you get stuck, try to remember what we did. And if you are really stuck, it's OK to check back here.
If I were to give you a final point to consider: try and see the connection between the code and the scene editor. Notice how everything that we have modified using the scene editor (like the text values or the placement of objects) can be changed in code too. Keep this in mind when you continue to add functionality. You can set things up in the Scene Editor and when the game runs, the code takes over. So if you want to implement certain behavior, think about:
- how can I get access to this game object in code
- what is the property (the
x
,text
, and so on) that I need to change
And finally, Phaser Editor has a part of the features available in the code, meaning that everything you can do in the editor, you can also do in code. And much more!
Thank you very much for reading and good luck on your game development journey!